Unit Test #
Unit test adalah fondasi utama dalam praktik software engineering modern. Hampir semua metodologi pengembangan perangkat lunak yang matang—mulai dari Agile, TDD (Test Driven Development), hingga Continuous Delivery—menempatkan unit test sebagai komponen krusial.
Artikel ini membahas unit test secara menyeluruh: mulai dari definisi, tujuan, karakteristik unit test yang baik, konsep mocking, hingga contoh implementasi menggunakan Kotlin, JUnit, dan Mockito. Artikel ini ditujukan untuk engineer yang ingin membangun kode yang robust, mudah dirawat, dan scalable.
Apa Itu Unit Test? #
Unit test adalah pengujian terhadap unit terkecil dari kode (biasanya fungsi atau method) secara terisolasi, untuk memastikan bahwa unit tersebut berperilaku sesuai dengan yang diharapkan.
Contoh unit:
- Satu fungsi perhitungan
- Satu method pada service
- Satu class dengan dependency yang dimock
Ciri utama unit test:
- Cepat dieksekusi (millisecond)
- Deterministik (hasil selalu sama)
- Tidak bergantung pada sistem eksternal (DB, API, filesystem)
Kenapa Unit Test Sangat Penting? #
Early Bug Detection #
Bug ditemukan saat development, bukan di production.
Safety Net untuk Refactoring #
Engineer dapat melakukan refactor dengan percaya diri karena ada test yang melindungi behavior.
Dokumentasi Hidup #
Unit test menjelaskan bagaimana kode seharusnya digunakan dan berperilaku.
Meningkatkan Design Code #
Kode yang mudah di-test biasanya:
- Lebih modular
- Lebih loosely coupled
- Lebih mengikuti SOLID principle
Karakteristik Unit Test yang Baik #
Unit test yang buruk sering kali menjadi beban. Unit test yang baik memiliki karakteristik berikut:
- Isolated – Tidak bergantung pada dependency nyata
- Fast – Ribuan test harus bisa dijalankan dalam hitungan detik
- Readable – Mudah dibaca dan dipahami
- Independent – Test tidak saling bergantung
- Focused – Satu test menguji satu behavior
Prinsip populer:
FIRST: Fast, Independent, Repeatable, Self-validating, Timely
Apa Itu Mocking? #
Mocking adalah teknik untuk menggantikan dependency nyata dengan object palsu (mock) yang behavior-nya dapat kita kontrol.
Dependency yang sering dimock:
- Database repository
- HTTP client / API eksternal
- Message queue
- Service lain
Tujuan mocking:
- Mengisolasi unit yang diuji
- Menghindari side effect
- Membuat test deterministik
Mock vs Stub vs Fake (Sekilas) #
| Tipe | Fungsi |
|---|---|
| Stub | Mengembalikan data statis |
| Mock | Bisa diverifikasi (dipanggil atau tidak) |
| Fake | Implementasi sederhana (in-memory) |
Mockito umumnya digunakan untuk mock dan stub sekaligus.
Contoh Kasus: User Service #
Tooling #
Dalam contoh ini kita menggunakan:
- Kotlin – Bahasa utama
- JUnit 5 – Testing framework
- Mockito + Mockito-Kotlin – Mocking framework
Dependency (Gradle):
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testImplementation("org.mockito:mockito-core:5.7.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
}
Struktur User Service #
Kita punya use case sederhana:
- Mengambil user dari repository
- Melakukan validasi
- Mengembalikan response
Domain Model #
data class User(
val id: String,
val name: String,
val isActive: Boolean
)
Repository Interface #
interface UserRepository {
fun findById(id: String): User?
}
Service yang Akan Diuji #
class UserService(private val userRepository: UserRepository) {
fun getActiveUserName(userId: String): String {
val user = userRepository.findById(userId)
?: throw IllegalArgumentException("User not found")
if (!user.isActive) {
throw IllegalStateException("User is not active")
}
return user.name
}
}
Unit Test Tanpa Mocking (Kenapa Buruk) #
Jika UserRepository menggunakan database sungguhan:
- Test jadi lambat
- Butuh setup data
- Tidak deterministik
Solusinya: mock repository.
Unit Test dengan JUnit dan Mockito #
Setup Test Class #
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.*
class UserServiceTest {
private lateinit var userRepository: UserRepository
private lateinit var userService: UserService
@BeforeEach
fun setup() {
userRepository = mock()
userService = UserService(userRepository)
}
User Aktif #
@Test
fun `should return user name when user is active`() {
val user = User("1", "Budi", true)
whenever(userRepository.findById("1"))
.thenReturn(user)
val result = userService.getActiveUserName("1")
assertEquals("Budi", result)
}
Yang diuji:
- Dependency dimock
- Fokus pada behavior service
User Tidak Ditemukan #
@Test
fun `should throw exception when user not found`() {
whenever(userRepository.findById("99"))
.thenReturn(null)
val exception = assertThrows(IllegalArgumentException::class.java) {
userService.getActiveUserName("99")
}
assertEquals("User not found", exception.message)
}
User Tidak Aktif #
@Test
fun `should throw exception when user is not active`() {
val user = User("2", "Ani", false)
whenever(userRepository.findById("2"))
.thenReturn(user)
val exception = assertThrows(IllegalStateException::class.java) {
userService.getActiveUserName("2")
}
assertEquals("User is not active", exception.message)
}
Verifikasi Interaksi dengan Mockito #
Mockito memungkinkan kita memverifikasi behavior:
verify(userRepository).findById("1")
verifyNoMoreInteractions(userRepository)
Ini berguna untuk:
- Memastikan side effect
- Mendeteksi logic yang tidak perlu
Best Practice Unit Test #
Jangan Test Framework #
Test behavior bisnis, bukan implementasi framework.
Hindari Over-Mocking #
Jika semua dimock, test kehilangan makna.
One Assert, One Behavior #
Satu test fokus ke satu tujuan.
Gunakan Naming yang Jelas #
shouldReturnUserNameWhenUserIsActive
Atau gaya Kotlin backtick:
`should return user name when user is active`
Unit Test vs Integration Test #
| Aspek | Unit Test | Integration Test |
|---|---|---|
| Dependency | Mock | Real |
| Kecepatan | Sangat cepat | Lebih lambat |
| Scope | Kecil | Lebih luas |
Keduanya saling melengkapi, bukan saling menggantikan.
Dalam TDD:
- Red – Tulis test gagal
- Green – Buat test lolos
- Refactor – Rapikan kode
Unit test bukan hasil sampingan, tapi driver design.
Dependency Injection dan Dampaknya terhadap Unit Test #
Apa Itu Dependency Injection (DI)? #
Dependency Injection (DI) adalah prinsip design di mana sebuah class tidak membuat dependency-nya sendiri, melainkan dependency tersebut disediakan dari luar (di-inject).
Intinya:
Jangan
newdependency di dalam class bisnis.
Dependency yang umum:
- Repository
- HTTP / API client
- Message broker / publisher
- External service
Contoh Tanpa Dependency Injection (Sulit Di-test) #
class UserService {
private val userRepository = UserRepositoryImpl()
fun getActiveUserName(userId: String): String {
val user = userRepository.findById(userId)
?: throw IllegalArgumentException("User not found")
if (!user.isActive) {
throw IllegalStateException("User is not active")
}
return user.name
}
}
Masalah utama:
- Dependency dibuat langsung di dalam class
- Tidak bisa diganti dengan mock
- Unit test terpaksa menyentuh resource nyata
Dengan Dependency Injection (Clean & Testable) #
class UserService(private val userRepository: UserRepository) {
fun getActiveUserName(userId: String): String {
val user = userRepository.findById(userId)
?: throw IllegalArgumentException("User not found")
if (!user.isActive) {
throw IllegalStateException("User is not active")
}
return user.name
}
}
Keuntungan:
- Dependency mudah dimock
- Unit test simpel
- Business logic lebih fokus
Dependency Injection dan Mocking #
val userRepository = mock<UserRepository>()
val userService = UserService(userRepository)
Mocking menjadi natural karena dependency tidak terikat implementasi konkret.
Constructor Injection vs Field Injection #
| Tipe Injection | Rekomendasi | Alasan |
|---|---|---|
| Constructor Injection | ✅ | Immutable, eksplisit, testable |
| Field Injection | ❌ | Sulit di-test tanpa framework |
Hubungan DI dengan SOLID #
DI adalah implementasi langsung dari Dependency Inversion Principle.
High-level module bergantung pada abstraction, bukan implementation.
Ringkasan (DI + Unit Test) #
Dependency Injection bukan hanya mempermudah unit test, tapi juga memperbaiki design secara keseluruhan.
Penutup #
Unit test bukan sekadar kewajiban, tapi alat strategis untuk:
- Menjaga kualitas kode
- Mempercepat development
- Mengurangi biaya bug di production
Dengan pemahaman mocking yang benar dan tooling yang tepat seperti JUnit dan Mockito, unit test menjadi investasi jangka panjang yang sangat bernilai bagi tim engineering.
Jika unit test terasa sulit, sering kali masalahnya bukan di test-nya—tapi di design kode-nya.