Coroutine memperkenalkan dimensi baru pada test: waktu. Sebuah suspend function yang memanggil delay(5000) akan membuat test berjalan 5 detik nyata jika dipakai runBlocking. Library kotlinx-coroutines-test mengatasi ini dengan runTest — blok yang menjalankan coroutine dengan virtual time, sehingga delay di-skip dan test selesai dalam milidetik. Chapter ini memperkenalkan runTest, TestDispatcher, dan pola umum untuk menguji suspend function dan structured concurrency.
runTest dan Virtual Time #
runTest adalah pengganti runBlocking untuk test. Bedanya: blok dijalankan pada TestScope dengan TestDispatcher yang mengontrol penjadwalan. Saat kode memanggil delay(1000), dispatcher tidak menunggu 1 detik nyata — ia memajukan waktu virtual sehingga coroutine langsung dilanjutkan. Dengan demikian, test yang secara logika menunggu total 10 detik dapat selesai dalam <10ms.
Tiga komponen utama:
| Komponen | Tujuan |
|---|---|
runTest { } | Blok test dengan virtual time |
TestScope | Scope yang dipakai di dalam runTest |
TestDispatcher (StandardTestDispatcher, UnconfinedTestDispatcher) | Kontrol eksekusi coroutine |
Pola runTest pada Produksi #
Bentuk asli test coroutine dengan kotlinx-coroutines-test:
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals
class AmbilDataTest {
private val svc = AmbilData()
@Test
fun `ambil pengguna dengan delay 100ms`() = runTest {
// virtual time: delay 100ms di-skip, test selesai instan
val hasil = svc.ambilPengguna(1)
assertEquals("Pengguna#1", hasil)
}
@Test
fun `ambil profil paralel`() = runTest {
val (nama, profil) = svc.ambilProfil(2)
assertEquals("Pengguna#2", nama)
assertEquals("Profil untuk #2", profil)
}
}
class AmbilData {
suspend fun ambilPengguna(id: Int): String {
delay(100L)
return "Pengguna#$id"
}
suspend fun ambilProfil(id: Int): Pair<String, String> = kotlinx.coroutines.coroutineScope {
val nama = kotlinx.coroutines.async { ambilPengguna(id) }
val profil = kotlinx.coroutines.async { delay(50L); "Profil untuk #$id" }
nama.await() to profil.await()
}
}Karena kotlinx-coroutines-test tidak tersedia pada environment kompilasi ini, berikut versi runnable yang memakai runBlocking (coroutine nyata) dengan komentar yang menjelaskan perbedaan durasi:
// coroutine_testing.kt
// Mendemonstrasikan pengujian coroutine dengan pola yang setara
// kotlinx-coroutines-test runTest. Karena file ini memakai
// kotlinx.coroutines, classpath ditambahkan otomatis.
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
class AmbilData {
suspend fun ambilPengguna(id: Int): String {
delay(100L) // simulasi I/O
return "Pengguna#$id"
}
suspend fun ambilProfil(id: Int): Pair<String, String> = coroutineScope {
val pengguna = async { ambilPengguna(id) }
val profil = async {
delay(50L)
"Profil untuk #$id"
}
pengguna.await() to profil.await()
}
}
// Virtual time: karena runBlocking menunggu delay nyata, kita ukur durasi.
// Pada runTest sebenarnya delay di-skip via TestDispatcher.
fun main() = runBlocking {
val ambil = AmbilData()
val mulai = System.nanoTime()
// Test 1: ambil satu pengguna
val pengguna = ambil.ambilPengguna(1)
check(pengguna == "Pengguna#1") { "Expected Pengguna#1, got $pengguna" }
println("[LULUS] ambilPengguna(1) = $pengguna")
// Test 2: ambil profil paralel
val (nama, profil) = ambil.ambilProfil(2)
check(nama == "Pengguna#2" && profil == "Profil untuk #2") {
"Hasil tak terduga: $nama / $profil"
}
println("[LULUS] ambilProfil(2) = ($nama, $profil)")
// Test 3: dengan dispatcher eksplisit
val hasil = withContext(Dispatchers.Default) {
"dieksekusi di Default"
}
println("[LULUS] withContext(Default) = $hasil")
// Test 4: launch anak
var counter = 0
val job = launch {
repeat(5) { i ->
counter = i + 1
}
}
job.join()
check(counter == 5) { "Counter expected 5, got $counter" }
println("[LULUS] launch repeat(5) -> counter=$counter")
val durasi = (System.nanoTime() - mulai) / 1_000_000
println()
println("Durasi nyata: ${durasi}ms (dengan runTest virtual time: <5ms)")
}Output yang Dihasilkan:
[LULUS] ambilPengguna(1) = Pengguna#1
[LULUS] ambilProfil(2) = (Pengguna#2, Profil untuk #2)
[LULUS] withContext(Default) = dieksekusi di Default
[LULUS] launch repeat(5) -> counter=5
Durasi nyata: 214ms (dengan runTest virtual time: <5ms)Mari kita amati beberapa poin penting:
delay(100L)diambilPengguna: padarunTest, delay ini di-skip — total test <5ms. PadarunBlockingdi contoh runnable, delay nyata 100ms tetap berlaku sehingga durasi tercatat ~200ms.coroutineScopedengan duaasync: dua suspend function berjalan paralel di dalam scope yang sama.await()mengembalikan hasil ke pemanggil. Total durasi nyata mendekati delay terlama (100ms), bukan penjumlahan (150ms).withContext(Dispatchers.Default): melompat ke thread pool default. PadarunTest, perpindahan dispatcher tetap diuji tetapi penjadwalannya tetap dalam virtual time.launch { ... }.join(): structured concurrency. Anak launch menyelesaikan counter sebelumjoin()mengembalikan kontrol ke pemanggil.
TestDispatcher: Standard vs Unconfined #
StandardTestDispatcher (default runTest) menjalankan coroutine pada antrean — anak coroutine tidak otomatis dijalankan saat diluncurkan, harus ada advanceUntilIdle() atau runCurrent(). Cocok untuk test yang ingin memeriksa state intermediate.
UnconfinedTestDispatcher menjalankan coroutine secara eager — anak launch langsung dieksekusi hingga delay atau titik suspend. Cocok untuk test yang ingin simulasi alur linier tanpa khawatir penjadwalan.
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.launch
class UnconfinedTest {
@Test
fun `urutan linier`() = runTest(UnconfinedTestDispatcher()) {
launch { /* langsung jalan */ }
}
}Kontrol Waktu Virtual #
TestScope menyediakan method advanceTimeBy(delay) dan advanceUntilIdle() yang memungkinkan kita memajukan waktu virtual secara manual. Pola ini berguna saat test ingin memeriksa state intermediate sebelum delay selesai. Misalnya, test yang memverifikasi bahwa progress bar muncul setelah 100ms dan hilang setelah 1000ms dapat memajukan virtual time ke 100ms, memeriksa state progress bar, lalu memajukan ke 1000ms dan memeriksa state akhir — semuanya tanpa menunggu nyata.
runCurrent() memaksa eksekusi coroutine yang sudah terjadwal tetapi belum dijalankan. Method ini berging saat ingin memverifikasi efek samping dari launch sebelum awaitComplete. Tanpa runCurrent(), anak launch pada StandardTestDispatcher belum tentu dieksekusi sebelum test keluar dari scope.
Konfigurasi Dependensi #
dependencies {
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
}Library ini menyertakan runTest, TestScope, kedua jenis TestDispatcher, dan implementasi TestCoroutineScheduler yang mengatur virtual time secara thread-safe.
Referensi Lanjutan #
- kotlinx-coroutines-test
— rujukan resmi
runTest,TestDispatcher, danadvanceUntilIdleuntuk pengujian suspend function
Selanjutnya #
Banyak API Kotlin modern mengembalikan Flow alih-alih nilai tunggal. Chapter flow-testing akan memperkenalkan library Turbine yang menyederhanakan pengujian emissions, completion, dan error Flow.