Pengujian kode KMP memiliki kebutuhan khusus: kita ingin menulis test sekali dan menjalankannya di setiap target. Bila test ditulis per source set spesifik, kita harus menduplikasi test yang sama untuk JVM, Android, dan iOS—salah satu masalah yang KMP coba selesaikan. Solusinya adalah source set commonTest yang berisi test untuk kode commonMain. Test di sini memakai library kotlin.test yang abstraksi terhadap framework per platform—JUnit di JVM, instrumentation test di Android, XCTest di iOS. Pada chapter ini kita akan memahami struktur commonTest, melihat simulasi test yang sama dijalankan di tiga target, dan mempelajari pola test yang baik untuk kode multiplatform.
Struktur commonTest #
Source set commonTest berisi test yang memverifikasi kode commonMain. Source set androidUnitTest, iosX64Test, dan jvmTest berisi test spesifik platform—misalnya test yang memakai API java.io.File hanya boleh berada di jvmTest. Kompilator Kotlin me-resolve commonTest per target: test yang sama dijalankan dengan runner JUnit di JVM, runner instrumentation di Android, dan runner XCTest di iOS.
kotlin {
sourceSets {
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-annotations-common"))
}
}
val androidUnitTest by getting {
dependencies {
implementation(kotlin("test-junit"))
}
}
}
}kotlin("test") adalah dependency meta yang di-resolve ke framework sesuai target. Di JVM menjadi JUnit 4 atau 5; di Android menjadi instrumentation runner; di iOS menjadi XCTest.
Library kotlin.test #
kotlin.test menyediakan assertion yang konsisten lintas platform: assertEquals(expected, actual), assertTrue(value), assertFailsWith<Exception> { ... }, dan assertNotNull(value). Assertion ini ditulis sekali dan berjalan di semua target tanpa modifikasi.
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class ValidatorEmailTest {
@Test
fun emailValid_diterima() {
assertTrue(ValidatorEmail.valid("[email protected]"))
}
@Test
fun emailTanpaDomain_ditolak() {
assertTrue(!ValidatorEmail.valid("budi@"))
}
}Simulasi Test di Tiga Target #
Demo berikut mensimulasikan pola commonTest dengan framework test ringan buatan sendiri. Test suite yang sama—ValidatorEmail, ValidatorTelepon, HitungPajak—dijalankan oleh tiga runner yang berbeda untuk mensimulasikan JUnit, instrumentation runner, dan XCTest.
// multiplatform_testing.kt
// commonTest source set: tulis test sekali, jalankan di setiap target.
// Demo: framework test ringan + eksekusi di tiga target tersimulasi.
// --- commonMain: kode yang akan diuji ---
data class Validasi(val aturan: List<(String) -> Boolean>) {
fun periksa(input: String): List<String> {
val hasil = mutableListOf<String>()
for ((i, rule) in aturan.withIndex()) {
if (!rule(input)) hasil.add("Aturan $i gagal untuk input: $input")
}
return hasil
}
}
object ValidatorEmail {
private val regex = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
fun valid(email: String): Boolean = regex.matches(email)
}
object ValidatorTelepon {
fun valid(telp: String): Boolean =
telp.all { it.isDigit() } && telp.length in 8..13
}
class HitungPajak(private val tarifPct: Double) {
fun hitung(harga: Long): Long {
require(harga >= 0) { "harga tidak boleh negatif" }
return ((harga * tarifPct) / 100.0).toLong()
}
}
// --- commonTest: framework test ringan (simulasi kotlin.test) ---
// kotlin.test menyediakan assertEquals, assertTrue, fail.
// Di sini kita implementasi sederhana dengan logger yang terhubung
// ke platform masing-masing (JvmTest, AndroidTest, IosTest).
data class HasilTest(val nama: String, val lulus: Boolean, val pesan: String)
class PengumpulTest(private val runner: String) {
private val hasil = mutableListOf<HasilTest>()
fun assertEquals(expected: Any?, actual: Any?, nama: String) {
val ok = expected == actual
hasil.add(HasilTest(nama, ok,
if (ok) "" else "expected=$expected actual=$actual"))
}
fun assertTrue(value: Boolean, nama: String) {
hasil.add(HasilTest(nama, value, if (value) "" else "expected true"))
}
fun assertThrows(aksi: () -> Unit, nama: String) {
val thrown = try { aksi(); false } catch (e: Throwable) { true }
hasil.add(HasilTest(nama, thrown, if (thrown) "" else "expected exception"))
}
fun cetakRingkasan(): List<HasilTest> {
println("--- Runner: $runner ---")
for (h in hasil) {
val status = if (h.lulus) "PASS" else "FAIL"
println(" [$status] ${h.nama}${if (h.pesan.isEmpty()) "" else " — ${h.pesan}"}")
}
val lulus = hasil.count { it.lulus }
val gagal = hasil.count { !it.lulus }
println(" Total: $lulus lulus, $gagal gagal")
println()
return hasil
}
}
// --- commonTest: definisi test yang dipakai di semua target ---
fun jalankanSuite(runner: String, target: String): List<HasilTest> {
val t = PengumpulTest(runner)
// Test ValidatorEmail
t.assertTrue(ValidatorEmail.valid("[email protected]"), "email valid budi")
t.assertTrue(ValidatorEmail.valid("[email protected]"), "email valid siti")
t.assertTrue(!ValidatorEmail.valid("bukan-email"), "email tolak plain text")
t.assertTrue(!ValidatorEmail.valid("budi@"), "email tolak tanpa domain")
// Test ValidatorTelepon
t.assertTrue(ValidatorTelepon.valid("081234567890"), "telp valid 12 digit")
t.assertTrue(!ValidatorTelepon.valid("08123"), "telp tolak 5 digit")
t.assertTrue(!ValidatorTelepon.valid("08abc"), "telp tolak ada huruf")
// Test HitungPajak
val pajak = HitungPajak(11.0)
t.assertEquals(11L, pajak.hitung(100), "pajak 11% dari 100 = 11")
t.assertEquals(0L, pajak.hitung(0), "pajak 11% dari 0 = 0")
t.assertEquals(1100L, pajak.hitung(10_000), "pajak 11% dari 10000 = 1100")
t.assertThrows({ pajak.hitung(-1) }, "pajak negatif harus throws")
// Test yang bergantung platform (mis. formatting angka)
t.assertEquals(
"11% dari 100",
"11% dari 100", // simulasi: format konsisten lintas platform
"format string konsisten lintas platform"
)
println("=== Target: $target ===")
return t.cetakRingkasan()
}
fun main() {
println("=== commonTest: test yang sama, tiga target ===\n")
val semuaHasil = mutableListOf<Pair<String, List<HasilTest>>>()
for ((runner, target) in listOf(
"JvmTestRunner" to "JVM (OpenJDK 21)",
"AndroidTestRunner" to "Android 14 (API 34)",
"IosTestRunner" to "iOS 17.2 (simulator)"
)) {
semuaHasil.add(target to jalankanSuite(runner, target))
}
// Ringkasan akhir
println("=== Ringkasan akhir ===")
var totalLulus = 0
var totalGagal = 0
for ((target, hasil) in semuaHasil) {
val l = hasil.count { it.lulus }
val g = hasil.count { !it.lulus }
println(" $target: $l lulus, $g gagal")
totalLulus += l
totalGagal += g
}
println(" TOTAL: $totalLulus lulus, $totalGagal gagal")
}Output yang Dihasilkan:
=== commonTest: test yang sama, tiga target ===
=== Target: JVM (OpenJDK 21) ===
--- Runner: JvmTestRunner ---
[PASS] email valid budi
[PASS] email valid siti
[PASS] email tolak plain text
[PASS] email tolak tanpa domain
[PASS] telp valid 12 digit
[PASS] telp tolak 5 digit
[PASS] telp tolak ada huruf
[PASS] pajak 11% dari 100 = 11
[PASS] pajak 11% dari 0 = 0
[PASS] pajak 11% dari 10000 = 1100
[PASS] pajak negatif harus throws
[PASS] format string konsisten lintas platform
Total: 12 lulus, 0 gagal
=== Target: Android 14 (API 34) ===
--- Runner: AndroidTestRunner ---
[PASS] email valid budi
[PASS] email valid siti
[PASS] email tolak plain text
[PASS] email tolak tanpa domain
[PASS] telp valid 12 digit
[PASS] telp tolak 5 digit
[PASS] telp tolak ada huruf
[PASS] pajak 11% dari 100 = 11
[PASS] pajak 11% dari 0 = 0
[PASS] pajak 11% dari 10000 = 1100
[PASS] pajak negatif harus throws
[PASS] format string konsisten lintas platform
Total: 12 lulus, 0 gagal
=== Target: iOS 17.2 (simulator) ===
--- Runner: IosTestRunner ---
[PASS] email valid budi
[PASS] email valid siti
[PASS] email tolak plain text
[PASS] email tolak tanpa domain
[PASS] telp valid 12 digit
[PASS] telp tolak 5 digit
[PASS] telp tolak ada huruf
[PASS] pajak 11% dari 100 = 11
[PASS] pajak 11% dari 0 = 0
[PASS] pajak 11% dari 10000 = 1100
[PASS] pajak negatif harus throws
[PASS] format string konsisten lintas platform
Total: 12 lulus, 0 gagal
=== Ringkasan akhir ===
JVM (OpenJDK 21): 12 lulus, 0 gagal
Android 14 (API 34): 12 lulus, 0 gagal
iOS 17.2 (simulator): 12 lulus, 0 gagal
TOTAL: 36 lulus, 0 gagalMari kita bedah bagian kunci dari kode di atas:
data class Validasi: kode dicommonMainyang akan diuji. Berisi daftar aturan berupa lambda—kode murni Kotlin tanpa API platform. Inilah tipe kode yang cocok untukcommonTest.ValidatorEmail,ValidatorTelepon: validator bisnis yang berada dicommonMain. MemakaiRegexyang tersedia di semua target KMP. Test untuk kedua validator hanya perlu ditulis sekali.HitungPajak: class denganrequire()untuk input validation. Test memverifikasi hasil perhitungan dan memastikan exception dilempar untuk input negatif.PengumpulTest: simulasi frameworkkotlin.test. MenyediakanassertEquals,assertTrue,assertThrows. Pada proyek nyata, runner ini digantikan JUnit (JVM), Espresso runner (Android), XCTest (iOS).jalankanSuite(runner, target): definisi test suite yang sama dijalankan untuk tiga runner. Inilah inticommonTest—kode test ditulis sekali, runner berbeda per target.- Output menunjukkan tiga run identik: 12 test PASS di setiap target. Bila satu test gagal di satu target saja, kemungkinan ada bug spesifik platform—misalnya
Localeformatting berbeda di iOS vs JVM.
Pola Test yang Baik untuk KMP #
Beberapa prinsip membantu menulis test KMP yang efektif. Pertama, letakkan test untuk kode commonMain di commonTest—tidak di androidUnitTest atau jvmTest. Kedua, hindari dependency ke library test spesifik platform di commonTest—misalnya Mockito hanya boleh di jvmTest. Ketiga, bila test membutuhkan resource platform (filesystem, database), pisahkan ke source set spesifik.
| Test yang cocok di commonTest | Test yang harus di source set spesifik |
|---|---|
| Validasi input | Akses filesystem |
| Perhitungan matematika | Panggilan network |
| Parsing string | UI rendering |
| Logika bisnis | Interaksi database |
Referensi Lanjutan #
- Run tests — panduan menjalankan unit test pada tiap target KMP
Penutup #
commonTest adalah source set yang melengkapi KMP—kode test ditulis sekali, dijalankan di setiap target. Library kotlin.test menyediakan assertion yang konsisten lintas platform. Dengan pola ini, satu bug yang muncul di satu platform dapat ditemukan tanpa menulis test terpisah per target. Chapter berikutnya akan masuk ke topik library KMP: bagaimana membangun library untuk dipublikasi ke Maven Central.