Unit Test

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:

  1. Isolated – Tidak bergantung pada dependency nyata
  2. Fast – Ribuan test harus bisa dijalankan dalam hitungan detik
  3. Readable – Mudah dibaca dan dipahami
  4. Independent – Test tidak saling bergantung
  5. 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) #

TipeFungsi
StubMengembalikan data statis
MockBisa diverifikasi (dipanggil atau tidak)
FakeImplementasi 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 #

AspekUnit TestIntegration Test
DependencyMockReal
KecepatanSangat cepatLebih lambat
ScopeKecilLebih luas

Keduanya saling melengkapi, bukan saling menggantikan.

Dalam TDD:

  1. Red – Tulis test gagal
  2. Green – Buat test lolos
  3. 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 new dependency 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 InjectionRekomendasiAlasan
Constructor InjectionImmutable, eksplisit, testable
Field InjectionSulit 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.

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact