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 #
| Konstruksi | Tujuan |
|---|---|
mockk<T>() | Membuat instance mock dari tipe T |
every { mock.foo(x) } returns y | Memprogram 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.
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 { }:
// 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:
Suhu di Bandung: 27 derajat
Verify OK: suhuKota(Bandung) dan simpan(Bandung, 27) dipanggil satu kali
Total panggilan tercatat: 2Mari kita bedah kunci implementasi mini-DSL tersebut:
MockK<T>class: perekam panggilan yang menyimpan daftar signature method yang pernah dipanggil, beserta daftar handler yang dapat diprogram denganevery { }.every(signature, hasil): mendaftarkan handler. Saatinvoke(signature)dipanggil, handler pertama yang cocok dipakai lalu dihapus — mensimulasikan behavior default MockK.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.verify(signature, count): menghitung berapa kali signature muncul di daftar panggilan dan menolak jika jumlah tidak cocok.MockApiCuaca: implementsApiCuacadengan delegasi keMockK.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.
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 #
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.
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 ..., danverify
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.