Saat sebuah class bergantung pada database, API eksternal, atau clock sistem, test yang memakai implementasi nyata menjadi lambat dan rapuh. Mocking menyelesaikan masalah ini dengan mengganti dependency asli dengan double yang perilakunya kita kontrol sepenuhnya. Chapter ini memperkenalkan konsep mock dan empat jenis test double — Stub, Mock, Spy, Fake — beserta panduan kapan masing-masing sebaiknya dipakai.

Apa Itu Mock #

Mock adalah objek pengganti dependency yang perilakunya kita program secara eksplisit. Alih-alih memanggil database nyata yang lambat, kita berikan objek yang mengembalikan nilai beku saat method findById dipanggil. Mock juga dapat merekam panggilan sehingga test dapat memverifikasi bahwa method dipanggil dengan argumen yang benar dan jumlah yang tepat.

Tiga alasan utama memakai mock:

AlasanPenjelasan
KecepatanMock mengembalikan nilai instan, database nyata butuh milidetik-detik
IsolasiTest fokus pada SUT, tidak terpengaruh perilaku dependency
DeterminismeDependency nyata dapat gagal karena jaringan; mock tidak

Empat Test Double #

Tidak semua double sama. Martin Fowler membedakan empat jenis yang sering dijumpai:

JenisPerilakuTujuan
StubMengembalikan nilai bekuMenyediakan input ke SUT
MockMerekam panggilan + diprogramMemverifikasi interaksi SUT terhadap dependency
SpyMembungkus asli + mencatatMemeriksa panggilan tanpa mengubah perilaku asli
FakeImplementasi sederhana berfungsiMensimulasikan dependency dengan state ringan

Pilihan tergantung tujuan test. Jika kita hanya butuh nilai kembalian tertentu, Stub cukup. Jika harus memastikan SUT memanggil method save dengan argumen spesifik, Mock cocok. Jika ingin memakai sebagian implementasi asli tetapi memantau interaksi, Spy. Jika butuh dependency yang berfungsi penuh tetapi ringan (tanpa I/O), Fake.

Implementasi Empat Jenis #

Kode berikut mendefinisikan keempat test double untuk UserRepository dan mendemonstrasikan perbedaan perilakunya:

kotlin
// mocking_fundamentals.kt
// Membandingkan empat test double: Stub, Mock, Spy, dan Fake.
// Kode ini runnable dengan kotlinc standar — tidak butuh MockK.

interface UserRepository {
    fun findById(id: Int): String?
    fun save(nama: String): Int
}

// === STUB === mengembalikan nilai beku, tidak peduli argumen.
class StubUserRepository : UserRepository {
    override fun findById(id: Int): String? = "Budi (stub)"
    override fun save(nama: String): Int = -1
}

// === MOCK === mencatat interaksi supaya bisa diverifikasi.
class MockUserRepository : UserRepository {
    var findByIdDipanggil = 0
        private set
    var saveDipanggil = 0
        private set
    var saveArgumenTerakhir: String? = null
        private set

    override fun findById(id: Int): String? {
        findByIdDipanggil++
        return null
    }
    override fun save(nama: String): Int {
        saveDipanggil++
        saveArgumenTerakhir = nama
        return 999
    }

    fun verifyFindById(dipanggil: Int) {
        check(findByIdDipanggil == dipanggil) {
            "Expected findById dipanggil $dipanggil, actual $findByIdDipanggil"
        }
    }
    fun verifySave(dipanggil: Int, arg: String) {
        check(saveDipanggil == dipanggil) { "Expected save dipanggil $dipanggil, actual $saveDipanggil" }
        check(saveArgumenTerakhir == arg) { "Expected save argumen $arg, actual $saveArgumenTerakhir" }
    }
}

// === SPY === membungkus implementasi nyata, mencatat panggilan.
class SpyUserRepository(private val asli: UserRepository) : UserRepository {
    var findByIdDipanggil = 0
        private set

    override fun findById(id: Int): String? {
        findByIdDipanggil++
        return asli.findById(id)
    }
    override fun save(nama: String): Int = asli.save(nama)
}

// === FAKE === implementasi sederhana yang berfungsi (in-memory).
class FakeUserRepository : UserRepository {
    private val data = mutableMapOf<Int, String>()
    private var urutan = 0
    override fun findById(id: Int): String? = data[id]
    override fun save(nama: String): Int {
        urutan++
        data[urutan] = nama
        return urutan
    }
    fun jumlahData(): Int = data.size
}

class UserService(private val repo: UserRepository) {
    fun salam(id: Int): String = "Halo, ${repo.findById(id) ?: "Tamu"}!"
    fun daftar(nama: String): Int = repo.save(nama)
}

fun main() {
    println("=== STUB ===")
    val stub = StubUserRepository()
    println(StubUserRepository().let { "save('${stub.save("test")}') -> -1 (beku)" })
    println()

    println("=== MOCK ===")
    val mock = MockUserRepository()
    val svcMock = UserService(mock)
    svcMock.daftar("Siti")
    svcMock.salam(1)
    mock.verifySave(1, "Siti")
    mock.verifyFindById(1)
    println("Verify OK: save(1, 'Siti'), findById(1)")
    println()

    println("=== SPY ===")
    val fake = FakeUserRepository()
    val spy = SpyUserRepository(fake)
    val svcSpy = UserService(spy)
    println(svcSpy.salam(1)) // Halo, Tamu!
    val idDewi = svcSpy.daftar("Dewi") // 1
    println("save('Dewi') -> id=$idDewi")
    println(svcSpy.salam(idDewi)) // Halo, Dewi!
    println("Spy: findByIdDipanggil=${spy.findByIdDipanggil}")
    println()

    println("=== FAKE ===")
    val svcFake = UserService(fake)
    val id = svcFake.daftar("Budi")
    println("save('Budi') -> id=$id")
    println(svcFake.salam(id))
    println("Fake menyimpan ${fake.jumlahData()} entri")
}

Output yang Dihasilkan:

text
=== STUB ===
save('-1') -> -1 (beku)

=== MOCK ===
Verify OK: save(1, 'Siti'), findById(1)

=== SPY ===
Halo, Tamu!
save('Dewi') -> id=1
Halo, Dewi!
Spy: findByIdDipanggil=2

=== FAKE ===
save('Budi') -> id=2
Halo, Budi!
Fake menyimpan 2 entri

Mari kita telaah perbedaan keempat implementasi:

  1. StubUserRepository: findById selalu mengembalikan "Budi (stub)" tanpa peduli argumen. save selalu mengembalikan -1. Tidak ada state yang diingat.
  2. MockUserRepository: mencatat berapa kali findById dan save dipanggil, serta argumen terakhir yang diterima. Method verifySave dan verifyFindById memeriksa jumlah panggilan.
  3. SpyUserRepository: membungkus implementasi asli (di sini FakeUserRepository) dan menambah penghitung panggilan tanpa mengubah hasil.
  4. FakeUserRepository: menyimpan data di MutableMap sehingga method save dan findById benar-benar berfungsi. State bertahan antar panggilan dalam test yang sama.

Kapan Memakai Mock #

Mock bukan solusi universal. Beberapa pedoman praktis:

  • Pakai mock ketika dependency eksternal lambat (database, jaringan), tidak stabil, atau tidak tersedia di lingkungan CI.
  • Hindari mock ketika dependency ringan dan deterministik — misalnya class kalkulator murni, atau koleksi in-memory. Mock pada kasus ini hanya menambah boilerplate tanpa manfaat isolasi.
  • Pertimbangkan Fake ketika dependency memiliki banyak method yang dipakai SUT — Fake yang berfungsi penuh lebih mudah dipelihara daripada Mock dengan banyak every { } returns.

Jebakan Mocking Berlebihan #

Mock yang berlebihan membuat test rapuh terhadap perubahan internal. Setiap kali implementasi SUT diubah (mis. method save dipanggil dua kali alih-alih satu kali), test mock gagal meskipun perilaku eksternal tetap benar. Aturan praktis: uji perilaku, bukan interaksi internal. Hanya verifikasi interaksi yang merupakan kontrak publik SUT — mis. “service harus memanggil audit.log saat transaksi terjadi”.

Referensi Lanjutan #

  • MockK documentation — dokumentasi MockK untuk memahami dasar mock object, mockk<T>(), every, dan verify

Selanjutnya #

Konsep mocking dapat diimplementasikan dengan tangan seperti di atas, tetapi library MockK menyediakan DSL yang jauh lebih ringkas untuk Kotlin. Chapter mockk akan memperkenalkan mockk<Class>(), every { } returns, dan verify { }.