Mocking

Dasar MockK dan mocking yang ramah coroutine.

Daftar isi

Mocking menggantikan dependency nyata suatu komponen dengan objek tiruan, sehingga pengujian unit dapat berjalan cepat dan terisolasi tanpa menyentuh basis data, jaringan, atau berkas. MockK adalah pustaka mocking untuk Kotlin yang mendukung fungsi biasa maupun fungsi suspend. Topik ini membahas pembuatan objek tiruan dengan mockk, penentuan jawaban melalui every dan coEvery, pemeriksaan interaksi dengan verify dan coVerify, serta perilaku ketat yang membedakan MockK dari banyak pustaka mocking lainnya.

Objek tiruan untuk dependency#

Contoh pada topik ini memakai antarmuka Gudang yang menyediakan dua operasi, yaitu simpan untuk fungsi biasa dan ambil untuk fungsi suspend. Kelas LayananPersediaan bergantung pada antarmuka tersebut, dan implementasi aslinya lazimnya menyentuh penyimpanan nyata. Ketika yang diuji adalah logika LayananPersediaan, implementasi nyata itu justru mengganggu karena pengujian menjadi lambat dan hasilnya bergantung pada isi penyimpanan.

Objek tiruan (mock) menyelesaikan masalah tersebut. Objek semacam ini dibuat dari antarmuka yang sama, tetapi setiap panggilannya dijawab sesuai stub yang ditentukan pengujian, bukan sesuai logika asli. Dengan begitu, perilaku dependency dapat diatur per kasus, termasuk kasus jarang seperti penyimpanan yang penuh atau data yang tidak ditemukan. Berkas berikut memuat seluruh contoh topik ini, dari antarmuka sampai pemakaian MockK.

kotlin main.kt
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.runBlocking

interface Gudang {
    fun simpan(nama: String): Boolean
    suspend fun ambil(id: Int): String?
}

class LayananPersediaan(private val gudang: Gudang) {
    fun catatMasuk(nama: String): Boolean = gudang.simpan(nama)
    suspend fun cekStok(id: Int): String? = gudang.ambil(id)
}

fun main() = runBlocking {
    val gudang = mockk<Gudang>()
    val layanan = LayananPersediaan(gudang)

    every { gudang.simpan("beras") } returns true
    coEvery { gudang.ambil(1) } returns "beras-2026"

    println("simpan beras: ${layanan.catatMasuk("beras")}")
    println("cek stok id 1: ${layanan.cekStok(1)}")

    verify { gudang.simpan("beras") }
    coVerify { gudang.ambil(1) }
    println("verifikasi selesai")
}

Menstub dengan every dan coEvery#

Fungsi mockk<Gudang>() membuat objek tiruan dari antarmuka Gudang. Secara default objek itu bersifat ketat, yaitu tidak ada satu pun panggilan yang dijawab sebelum jawabannya dideklarasikan. Deklarasi jawaban disebut stubbing, dan MockK menuliskannya dalam bentuk blok lambda yang memanggil metode target persis seperti pemanggilan aslinya. Baris every { gudang.simpan("beras") } returns true menyatakan bahwa pemanggilan simpan dengan argumen beras mengembalikan true.

Fungsi ambil dideklarasikan sebagai suspend, sehingga stub-nya tidak boleh ditulis dengan every. Pustaka menyediakan coEvery untuk fungsi suspend, dan bentuk pemakaiannya identik. Apabila every dipakai pada fungsi suspend, pustaka menolaknya saat runtime karena blok tersebut tidak berjalan di dalam coroutine. Blok main membungkus seluruh pekerjaan dengan runBlocking agar pemanggilan suspend pada layanan dapat ditulis sekuensial.

Perintah berikut mengompilasi berkas lalu menjalankannya dengan classpath MockK yang lengkap, termasuk pustaka coroutine dan tiga flag java yang menjaga pemuatan agent MockK tetap bebas peringatan.

bash
export PATH=/home/kal/my-project/tools/kotlinc/bin:/home/kal/my-project/tools/jdk-merged/bin:$PATH
export LIBS=/home/kal/my-project/tools/libs
export STDLIB=/home/kal/my-project/tools/kotlinc/lib/kotlin-stdlib.jar
# classpath memuat MockK beserta seluruh dependensinya
export CP="$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar:$LIBS/mockk-jvm-1.14.11.jar:$LIBS/mockk-dsl-jvm-1.14.11.jar:$LIBS/mockk-agent-jvm-1.14.11.jar:$LIBS/mockk-agent-api-jvm-1.14.11.jar:$LIBS/mockk-core-jvm-1.14.11.jar:$LIBS/objenesis-3.4.jar:$LIBS/paranamer-2.8.jar:$LIBS/kotlin-reflect-2.2.21.jar:$LIBS/byte-buddy-1.18.17.jar:$LIBS/byte-buddy-agent-1.18.17.jar"
# kompilasi program yang memakai MockK
kotlinc main.kt -cp "$CP" -d out.jar
# tiga flag java membuat pemuatan agent MockK bebas peringatan
java -Xshare:off -XX:+EnableDynamicAgentLoading -Djdk.attach.allowAttachSelf=true -cp "out.jar:$CP:$STDLIB" MainKt
text Keluaran
simpan beras: true
cek stok id 1: beras-2026
verifikasi selesai

Nilai pada keluaran seluruhnya berasal dari stub, bukan dari logika penyimpanan. catatMasuk mengembalikan true karena every menyatakan demikian, sedangkan cekStok mengembalikan string beras-2026 yang ditetapkan coEvery. Jika implementasi Gudang diganti dengan versi nyata, keluaran yang sama hanya terjadi bila implementasi itu kebetulan berperilaku identik, dan itulah alasan objek tiruan menjadikan pengujian dapat diprediksi.

Memverifikasi interaksi dengan verify#

Asersi memeriksa nilai yang dihasilkan, sedangkan verifikasi memeriksa interaksi yang terjadi, misalnya apakah sebuah metode benar-benar dipanggil. Baris verify { gudang.simpan("beras") } menuntut pemanggilan tersebut terjadi tepat satu kali, yang merupakan jumlah default, dan coVerify adalah padanannya untuk fungsi suspend. Bila tuntutan itu tidak terpenuhi, MockK melempar exception dan pengujian gagal.

Jumlah pemanggilan yang diharapkan dapat ditentukan secara eksplisit dengan parameter exactly, sebagaimana ditunjukkan potongan berikut. Verifikasi semacam ini berguna ketika keluaran fungsi tidak cukup membuktikan perilakunya, misalnya metode yang mengembalikan Unit tetapi efeknya ada di dependency.

kotlin Contoh: verifikasi dengan jumlah eksplisit
// tidak-dijalankan
verify(exactly = 2) { gudang.simpan("gandum") }

Perilaku ketat dan pilihan relaxed#

Pada objek ketat, setiap panggilan yang belum distub melempar exception MockKException berisi detail argumennya. Pada contoh di atas, gudang hanya distub untuk ambil(1), sehingga pemanggilan layanan.cekStok(2) langsung menghentikan program dengan pesan yang jelas. Sifat ini sering dimanfaatkan sebagai pengaman, karena pengujian otomatis gagal saat kode menyentuh jalur yang belum diperhitungkan.

Apabila detail jawaban tidak penting bagi pengujian, objek dapat dibuat dengan mockk<Gudang>(relaxed = true). Objek relaxed menjawab setiap panggilan yang belum distub dengan nilai default, misalnya false untuk Boolean dan null untuk tipe yang bisa dibilang nol, sehingga hanya kasus penting yang perlu distub. Kebalikan sifatnya adalah pengujian menjadi kurang ketat, karena lupa menstub tidak lagi terdeteksi.

Ringkasan#

  • Objek tiruan menggantikan dependency nyata agar pengujian unit cepat, terisolasi, dan hasilnya dapat diprediksi.
  • mockk membuat objek ketat yang jawabannya ditentukan lewat every, sedangkan coEvery dipakai untuk fungsi suspend.
  • verify dan coVerify memeriksa pemanggilan yang terjadi, dengan jumlah default satu kali dan parameter exactly untuk jumlah lainnya.
  • Mode relaxed menjawab panggilan yang belum distub dengan nilai default, sedangkan mode ketat melempar MockKException.

Referensi#

  • MockK — MockK Team — Dokumentasi resmi MockK, mencakup stubbing, verifikasi, dan dukungan coroutine.