Pengujian adalah bagian tak terpisahkan dari pengembangan peranti lunak yang sehat. Ekosistem testing Kotlin matang dengan banyak pilihan: kotlin-test untuk assertion dasar lintas platform, JUnit 5 sebagai runner JVM standar, MockK untuk mocking idiomatik Kotlin, Testcontainers untuk integration test dengan Docker, dan Turbine untuk menguji Flow coroutine. Pada chapter ini kita memetakan tooling, mendemokan pola Arrange-Act-Assert dengan mini test runner, dan menjelaskan tipe test yang umum dipakai.
Tooling Testing Kotlin #
| Tool | Peran | Status |
|---|---|---|
| kotlin-test | Assertion lintas platform (JVM, JS, Native) | Resmi JetBrains |
| JUnit 5 | Runner test JVM dengan @Test, @ParameterizedTest | Standar industri |
| MockK | Mock library idiomatik Kotlin (mockk, every, verify) | Komunitas |
| Testcontainers | Docker container untuk integration test (DB, Redis, Kafka) | Komunitas |
| Turbine | Test Flow coroutine (awaitItem, awaitComplete) | Cash App |
| Kotest | Framework alternatif, property-based, BDD style | Komunitas |
| Strikt | Assertion fluent (expectThat) | Komunitas |
Demo Mini Test Runner #
Cuplikan berikut membangun mini test runner dengan assertion helper manual. Pola yang ditunjukkan — SUT, fake repository, dan assertion — sama dengan yang dipakai JUnit 5 + MockK di project nyata:
// panduan_testing.kt
// Tour singkat Panduan Testing Kotlin:
// - kotlin-test: https://kotlinlang.org/api/latest/kvm/stdlib/kotlin.test/
// - JUnit 5: https://junit.org/junit5/
// - MockK: https://mockk.io
// - Testcontainers: https://testcontainers.com
// - Turbine: https://github.com/cashapp/turbine
//
// File ini mendemokan pola testing tanpa framework eksternal —
// assertion manual dan test runner sederhana.
import java.util.concurrent.atomic.AtomicInteger
// === SUT (System Under Test) ===
class Kalkulator {
fun tambah(a: Int, b: Int): Int = a + b
fun bagi(a: Int, b: Int): Result<Int> =
if (b == 0) Result.failure(IllegalArgumentException("tidak bisa bagi nol"))
else Result.success(a / b)
}
class PenggunaService(private val repo: PenggunaRepo) {
fun ambilNama(id: Int): String = repo.cari(id)?.nama ?: "Tidak dikenal"
}
interface PenggunaRepo {
fun cari(id: Int): Pengguna?
}
data class Pengguna(val id: Int, val nama: String)
// === Fake/Mock implementation ===
class PenggunaRepoFake(private val data: Map<Int, Pengguna> = emptyMap()) : PenggunaRepo {
val callCount = AtomicInteger(0)
override fun cari(id: Int): Pengguna? {
callCount.incrementAndGet()
return data[id]
}
}
// === Mini test runner ===
class TestRunner {
private val tests = mutableListOf<Pair<String, () -> Unit>>()
private var passed = 0
private var failed = 0
fun test(nama: String, block: () -> Unit) {
tests.add(nama to block)
}
fun run() {
tests.forEach { (nama, block) ->
try {
block()
println(" [PASS] $nama")
passed++
} catch (e: Throwable) {
println(" [FAIL] $nama: ${e.message}")
failed++
}
}
println()
println("Hasil: $passed lulus, $failed gagal, dari ${tests.size} total")
}
}
// === Assertion helpers ===
fun assertEquals(expected: Any?, actual: Any?) {
if (expected != actual) {
throw AssertionError("expected=$expected, actual=$actual")
}
}
fun assertTrue(actual: Boolean, msg: String = "") {
if (!actual) throw AssertionError("expected true: $msg")
}
fun <T> assertResult(expected: T, result: Result<T>) {
if (result.getOrElse { null } != expected) {
throw AssertionError("expected=$expected, result=$result")
}
}
fun main() {
println("=== Panduan Testing Kotlin ===")
println("kotlin-test | JUnit 5 | MockK | Testcontainers | Turbine")
println()
println("--- 1. Unit test Kalkulator ---")
val runner = TestRunner()
val k = Kalkulator()
runner.test("tambah positif") { assertEquals(5, k.tambah(2, 3)) }
runner.test("tambah negatif") { assertEquals(-1, k.tambah(2, -3)) }
runner.test("bagi normal") { assertResult(3, k.bagi(6, 2)) }
runner.test("bagi nol error") {
val r = k.bagi(6, 0)
assertTrue(r.isFailure, "harus failure")
}
runner.run()
println()
println("--- 2. Service dengan fake repository ---")
val repo = PenggunaRepoFake(mapOf(1 to Pengguna(1, "Budi")))
val svc = PenggunaService(repo)
val runner2 = TestRunner()
runner2.test("ambil pengguna dikenal") {
assertEquals("Budi", svc.ambilNama(1))
}
runner2.test("ambil pengguna tidak dikenal") {
assertEquals("Tidak dikenal", svc.ambilNama(99))
}
runner2.test("repo dipanggil") {
repo.callCount.set(0)
svc.ambilNama(1)
svc.ambilNama(99)
assertEquals(2, repo.callCount.get())
}
runner2.run()
println()
println("--- 3. Tooling populer ---")
val tools = listOf(
"kotlin-test -> assertion dasar (cross-platform)",
"JUnit 5 -> runner test populer di JVM, @Test, @ParameterizedTest",
"MockK -> mock library idiomatik Kotlin (mockk, every, verify)",
"Testcontainers -> Docker container untuk integration test (DB, Redis, Kafka)",
"Turbine -> test Flow coroutine (awaitItem, awaitComplete)",
"Kotest -> framework test alternatif, property-based, BDD style",
"Strikt -> assertion fluent (expectThat(x).isEqualTo(y))",
"Fixture Monkey -> generate test data otomatis"
)
tools.forEach { println(" $it") }
println()
println("--- 4. Anatomi test baik ---")
val anatomi = listOf(
"1. Arrange — siapkan SUT dan dependency (fake/mock)",
"2. Act — panggil method yang diuji",
"3. Assert — verifikasi hasil dengan assertion",
"4. Cleanup — reset state (bila perlu)"
)
anatomi.forEach { println(" $it") }
println()
println("--- 5. Tipe test ---")
val tipe = listOf(
"Unit test -> 1 class/function, dependency di-mock",
"Integration test -> beberapa class bersama, dependency asli",
"End-to-end test -> seluruh sistem via HTTP/CLI",
"Smoke test -> cek dasar (aplikasi jalan, health OK)",
"Performance test -> ukur throughput, latency (JMH, Gatling)",
"Property test -> cek properti (mis. reverse(reverse(x)) == x)"
)
tipe.forEach { println(" $it") }
println()
println("--- 6. Konfigurasi Gradle ---")
println(" dependencies {")
println(" testImplementation(kotlin(\"test\"))")
println(" testImplementation(\"org.junit.jupiter:junit-jupiter:5.10.2\")")
println(" testImplementation(\"io.mockk:mockk:1.13.10\")")
println(" testImplementation(\"org.testcontainers:junit-jupiter:1.19.7\")")
println(" testImplementation(\"app.cash.turbine:turbine:1.0.0\")")
println(" }")
println(" tasks.test { useJUnitPlatform() }")
}Output yang Dihasilkan:
=== Panduan Testing Kotlin ===
kotlin-test | JUnit 5 | MockK | Testcontainers | Turbine
--- 1. Unit test Kalkulator ---
[PASS] tambah positif
[PASS] tambah negatif
[PASS] bagi normal
[PASS] bagi nol error
Hasil: 4 lulus, 0 gagal, dari 4 total
--- 2. Service dengan fake repository ---
[PASS] ambil pengguna dikenal
[PASS] ambil pengguna tidak dikenal
[PASS] repo dipanggil
Hasil: 3 lulus, 0 gagal, dari 3 total
--- 3. Tooling populer ---
kotlin-test -> assertion dasar (cross-platform)
JUnit 5 -> runner test populer di JVM, @Test, @ParameterizedTest
MockK -> mock library idiomatik Kotlin (mockk, every, verify)
Testcontainers -> Docker container untuk integration test (DB, Redis, Kafka)
Turbine -> test Flow coroutine (awaitItem, awaitComplete)
Kotest -> framework test alternatif, property-based, BDD style
Strikt -> assertion fluent (expectThat(x).isEqualTo(y))
Fixture Monkey -> generate test data otomatis
--- 4. Anatomi test baik ---
1. Arrange — siapkan SUT dan dependency (fake/mock)
2. Act — panggil method yang diuji
3. Assert — verifikasi hasil dengan assertion
4. Cleanup — reset state (bila perlu)
--- 5. Tipe test ---
Unit test -> 1 class/function, dependency di-mock
Integration test -> beberapa class bersama, dependency asli
End-to-end test -> seluruh sistem via HTTP/CLI
Smoke test -> cek dasar (aplikasi jalan, health OK)
Performance test -> ukur throughput, latency (JMH, Gatling)
Property test -> cek properti (mis. reverse(reverse(x)) == x)
--- 6. Konfigurasi Gradle ---
dependencies {
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testImplementation("io.mockk:mockk:1.13.10")
testImplementation("org.testcontainers:junit-jupiter:1.19.7")
testImplementation("app.cash.turbine:turbine:1.0.0")
}
tasks.test { useJUnitPlatform() }Perhatikan dua batch test yang berjalan: empat test Kalkulator dan tiga test PenggunaService. Semua lulus karena assertion sesuai; bila ada yang gagal, mini runner akan mencetak [FAIL] dengan pesan error.
Anatomi dan Tipe Test #
Test yang baik selalu mengikuti pola Arrange-Act-Assert. Bagian Arrange menyiapkan SUT (System Under Test) dan dependency (fake atau mock). Bagian Act memanggil method yang diuji. Bagian Assert memverifikasi hasil dengan assertion. Tipe test berkisar dari unit test (satu class dengan dependency di-mock) sampai end-to-end test (seluruh sistem via HTTP). Property test berbeda: alih-alih contoh konkret, kita berikan properti umum (mis. reverse(reverse(x)) == x) dan framework seperti Kotest Property men-generate ratusan input acak untuk memverifikasi.
Referensi Lanjutan #
- Kotlin stdlib API reference
— rujukan stdlib Kotlin yang mencakup paket
kotlin.testuntuk assertion lintas platform
Selanjutnya #
Test yang sehat menjamin kualitas kode, namun kualitas tidak lengkap tanpa keamanan. Chapter berikutnya membahas panduan keamanan — OWASP Top 10, kriptografi, dan ProGuard/R8 untuk Android.