MockK adalah library mocking idiomatik untuk Kotlin yang memanfaatkan fitur bahasa seperti lambda dengan receiver dan inline function untuk menyediakan DSL yang ringkas. Berbeda dari Mockito yang dirancang untuk Java, MockK mendukung secara native fitur Kotlin seperti coroutine, suspend function, top-level function, dan object declaration. Chapter ini memperkenalkan tiga pilar MockK: mockk<T>() untuk membuat mock, every { } returns untuk memprogram perilaku, dan verify { } untuk memeriksa interaksi.

Tiga Pilar MockK #

KonstruksiTujuan
mockk<T>()Membuat instance mock dari tipe T
every { mock.foo(x) } returns yMemprogram mock: panggilan foo(x) mengembalikan y
verify { mock.foo(x) }Memverifikasi bahwa foo(x) pernah dipanggil
coEvery { } / coVerify { }Varian untuk suspend function (coroutine)

MockK memakai lambda dengan receiver sehingga sintaks every { api.suhuKota("Bandung") } returns 27 terasa seperti kalimat: “setiap kali suhuKota Bandung dipanggil, kembalikan 27”.

Penulisan dengan MockK #

Bentuk asli test MockK pada produksi terlihat seperti berikut. Kode ini butuh dependensi io.mockk:mockk agar kompilasi berhasil.

kotlin Contoh: test MockK untuk LayananCuaca
import io.mockk.mockk
import io.mockk.every
import io.mockk.verify
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals

class LayananCuacaTest {
    private val api: ApiCuaca = mockk()
    private val layanan = LayananCuaca(api)

    @Test
    fun `ringkasan mengembalikan suhu dari api`() {
        // every { } returns
        every { api.suhuKota("Bandung") } returns 27
        every { api.simpan("Bandung", 27) } returns true

        val hasil = layanan.ringkasan("Bandung")

        assertEquals("Suhu di Bandung: 27 derajat", hasil)
        // verify { } dipanggil satu kali
        verify(exactly = 1) { api.suhuKota("Bandung") }
        verify { api.simpan("Bandung", 27) }
    }
}

interface ApiCuaca {
    fun suhuKota(nama: String): Int
    fun simpan(kota: String, suhu: Int): Boolean
}

class LayananCuaca(private val api: ApiCuaca) {
    fun ringkasan(kota: String): String {
        val suhu = api.suhuKota(kota)
        api.simpan(kota, suhu)
        return "Suhu di $kota: $suhu derajat"
    }
}

Untuk demonstrasi runnable tanpa dependensi MockK, berikut simulasi mini-DSL yang meniru every { } returns dan verify { }:

kotlin
// mockk_demo.kt
// Mendemonstrasikan pola MockK (mockk<Class>(), every { } returns,
// verify { }) dengan mini-DSL yang runnable tanpa dependensi MockK.

interface ApiCuaca {
    fun suhuKota(nama: String): Int
    fun simpan(kota: String, suhu: Int): Boolean
}

// Mock minimal ala MockK: perekam pemanggilan + handler yang dapat di-override.
class MockK<T> {
    private val panggilan = mutableListOf<String>()
    private val handler = mutableMapOf<String, MutableList<() -> Any?>>()

    fun every(signature: String, hasil: () -> Any?) {
        handler.getOrPut(signature) { mutableListOf() }.add(hasil)
    }

    @Suppress("UNCHECKED_CAST")
    fun invoke(signature: String, vararg args: Any?): Any? {
        panggilan += signature
        val daftar = handler[signature]
        return if (daftar.isNullOrEmpty()) null else daftar.removeAt(0)()
    }

    fun verify(signature: String, count: Int = 1) {
        val actual = panggilan.count { it == signature }
        check(actual == count) {
            "Verify gagal: $signature expected=$count actual=$actual"
        }
    }

    fun jumlahPanggilan(): Int = panggilan.size
}

class MockApiCuaca(private val mock: MockK<Any?>) : ApiCuaca {
    override fun suhuKota(nama: String): Int =
        mock.invoke("suhuKota($nama)") as? Int ?: 0
    override fun simpan(kota: String, suhu: Int): Boolean =
        mock.invoke("simpan($kota, $suhu)") as? Boolean ?: false
}

// SUT (System Under Test) yang bergantung pada ApiCuaca
class LayananCuaca(private val api: ApiCuaca) {
    fun ringkasan(kota: String): String {
        val suhu = api.suhuKota(kota)
        api.simpan(kota, suhu)
        return "Suhu di $kota: $suhu derajat"
    }
}

fun main() {
    // mockk<ApiCuaca>()
    val mock = MockK<Any?>()
    val apiMock = MockApiCuaca(mock)

    // every { apiMock.suhuKota("Bandung") } returns 27
    mock.every("suhuKota(Bandung)") { 27 }
    // every { apiMock.simpan(any(), any()) } returns true
    mock.every("simpan(Bandung, 27)") { true }

    val layanan = LayananCuaca(apiMock)

    // Act
    val ringkasan = layanan.ringkasan("Bandung")
    println(ringkasan)

    // verify { apiMock.suhuKota("Bandung") dipanggil sekali }
    mock.verify("suhuKota(Bandung)", 1)
    mock.verify("simpan(Bandung, 27)", 1)
    println("Verify OK: suhuKota(Bandung) dan simpan(Bandung, 27) dipanggil satu kali")
    println("Total panggilan tercatat: ${mock.jumlahPanggilan()}")
}

Output yang Dihasilkan:

text
Suhu di Bandung: 27 derajat
Verify OK: suhuKota(Bandung) dan simpan(Bandung, 27) dipanggil satu kali
Total panggilan tercatat: 2

Mari kita bedah kunci implementasi mini-DSL tersebut:

  1. MockK<T> class: perekam panggilan yang menyimpan daftar signature method yang pernah dipanggil, beserta daftar handler yang dapat diprogram dengan every { }.
  2. every(signature, hasil): mendaftarkan handler. Saat invoke(signature) dipanggil, handler pertama yang cocok dipakai lalu dihapus — mensimulasikan behavior default MockK.
  3. invoke(signature, vararg args): dipanggil oleh method mock. Perekam menambahkan signature ke daftar panggilan lalu memanggil handler. Pada MockK asli, ini dilakukan via bytecode instrumentation.
  4. verify(signature, count): menghitung berapa kali signature muncul di daftar panggilan dan menolak jika jumlah tidak cocok.
  5. MockApiCuaca: implements ApiCuaca dengan delegasi ke MockK.invoke(...). Inilah yang disebut “mock instance” — setiap method di-override untuk memanggil mock engine.

coEvery untuk Suspend Function #

MockK menyediakan coEvery { } dan coVerify { } untuk suspend function. Sintaks identik dengan every { } tetapi dipakai di blok coroutine.

kotlin Contoh: mock suspend function
import io.mockk.mockk
import io.mockk.coEvery
import io.mockk.coVerify
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals

class AmbilDataTest {
    private val api: ApiData = mockk()
    private val svc = AmbilData(api)

    @Test
    fun `ambil data async`() = runBlocking {
        coEvery { api.ambil(1) } returns "Pengguna#1"
        val hasil = svc.ambilSync(1)
        assertEquals("Pengguna#1", hasil)
        coVerify { api.ambil(1) }
    }
}

relaxed = true #

Mock yang dibuat dengan mockk<T>() (tanpa relaxed) akan melempar MockKException saat method dipanggil tanpa every { } diprogram. Opsi mockk<T>(relaxed = true) membuat mock mengembalikan nilai default — 0 untuk Int, null untuk nullable, mock rekursif untuk tipe objek. Pakai relaxed untuk test cepat, tetapi waspadai: mock relaxed dapat menyembunyikan bug saat kita lupa memprogram method yang seharusnya penting.

Konfigurasi Dependensi #

kotlin Contoh: dependensi MockK
dependencies {
    testImplementation("io.mockk:mockk:1.13.12")
}

MockK tidak memerlukan agent khusus pada JVM modern — library memakai inline function dan reflective access untuk membuat mock tanpa instrumentasi bytecode. Pada Kotlin/Native dan Kotlin/JS, MockK tersedia versi terbatas yang mendukung sebagian besar API.

Spy Object #

Selain mockk<T>(), MockK menyediakan spyk(obj) yang membungkus instance nyata dengan kemampuan mock. Method yang tidak diprogram akan memanggil implementasi asli, sementara method yang diprogram dapat diganti perilakunya. Pola ini cocok ketika sebagian besar method objek ingin diuji secara nyata tetapi beberapa method spesifik perlu dikontrol.

kotlin Contoh: spyk untuk sebagian override
import io.mockk.spyk
import io.mockk.every
import org.junit.jupiter.api.Test

class LayananTest {
    @Test
    fun `sebagian method di-mock`() {
        val asli = LayananNyata()
        val spy = spyk(asli)
        every { spy.ambilVersi() } returns "1.0-mock"

        // methodAmbilVersi mengembalikan nilai mock
        // methodLain memanggil implementasi asli
    }
}

Referensi Lanjutan #

  • MockK documentation — dokumentasi resmi MockK mencakup mock object, spy, every { ... } returns ..., dan verify

Selanjutnya #

MockK mengotomatiskan pembuatan mock, tetapi tidak selalu mock adalah pilihan terbaik. Chapter stubs-mocks-dan-fakes akan membedah taksonomi test double secara lebih dalam dan membandingkan kapan memakai Stub, Fake, atau Mock.