Assertion adalah jantung setiap test — pernyataan eksplisit tentang kondisi yang harus benar agar test dianggap lulus. Tanpa assertion, test hanyalah eksekusi kode tanpa verifikasi. Chapter ini memperkenalkan tujuh assertion paling sering dipakai, dari yang paling sederhana (assertEquals) hingga assertAll dari JUnit 5 yang mengevaluasi semua assertion dalam satu grup meskipun ada yang gagal.

Tujuh Assertion Penting #

AssertionTujuan
assertEquals(expected, actual)Memastikan dua nilai sama
assertNotEquals(unexpected, actual)Memastikan dua nilai berbeda
assertTrue(condition)Memastikan boolean bernilai true
assertNull(value)Memastikan referensi null
assertNotNull(value)Memastikan referensi tidak null
assertThrows<E> { ... }Memastikan blok melemparkan pengecualian tipe E
assertAll(executables...)Menjalankan semua assertion dalam grup; semua dilaporkan

Pilihan assertion memengaruhi kejelasan test. assertEquals(4, hasil) lebih ekspresif daripada assertTrue(hasil == 4) karena pembaca langsung tahu apa yang dibandingkan. Untuk nullability, assertNull(x) lebih jelas daripada assertTrue(x == null).

Kode Asli dengan JUnit 5 #

Berikut bentuk asli assertion JUnit 5 yang dipakai pada produksi. Runner JUnit menangkap kegagalan dan melaporkan baris mana yang gagal.

kotlin Contoh: assertion JUnit 5 lengkap
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*

class PenggunaTest {
    @Test
    fun `validasi pengguna Budi`() {
        val budi = Pengguna("Budi")
        assertEquals("Budi", budi.nama)
        assertNotEquals("Siti", budi.nama)
        assertTrue(budi.nama.startsWith("B"))
        assertNull(null)
        assertNotNull(budi)

        val exc = assertThrows<IllegalArgumentException> {
            require(1 > 5) { "tidak mungkin" }
        }
        assertEquals("tidak mungkin", exc.message)

        // assertAll: semua assertion dijalankan, lalu laporan dikumpulkan
        assertAll("validasi lengkap Budi",
            { assertEquals(4, budi.nama.length) },
            { assertTrue(budi.nama.isNotEmpty()) }
        )
    }
}
data class Pengguna(val nama: String)

Untuk demonstrasi runnable tanpa dependensi JUnit, berikut versi yang meniru API ini menggunakan implementasi lokal:

kotlin
// assertions_demo.kt
// Mendemonstrasikan assertion umum (assertEquals, assertNotEquals,
// assertTrue, assertNull, assertNotNull, assertThrows) dan assertAll
// dari JUnit 5 untuk pengelompokan beberapa assertion sekaligus.

object Assert {
    private var pass = 0
    private var fail = 0

    private fun ok(label: String, detail: String) { println("[LULUS] $label — $detail"); pass++ }
    private fun no(label: String, detail: String) { println("[GAGAL]  $label — $detail"); fail++ }

    fun <T> assertEquals(expected: T, actual: T, msg: String = "assertEquals") {
        if (expected == actual) ok(msg, "$actual") else no(msg, "expected=$expected actual=$actual")
    }

    fun <T> assertNotEquals(expected: T, actual: T, msg: String = "assertNotEquals") {
        if (expected != actual) ok(msg, "actual=$actual") else no(msg, "nilai sama $actual")
    }

    fun assertTrue(kondisi: Boolean, msg: String = "assertTrue") {
        if (kondisi) ok(msg, "true") else no(msg, "kondisi false")
    }

    fun assertNull(value: Any?, msg: String = "assertNull") {
        if (value == null) ok(msg, "null") else no(msg, "value=$value")
    }

    fun assertNotNull(value: Any?, msg: String = "assertNotNull") {
        if (value != null) ok(msg, "value=$value") else no(msg, "value=null")
    }

    fun assertThrows(exceptionClass: kotlin.reflect.KClass<*>, blok: () -> Unit, msg: String = "assertThrows") {
        try {
            blok()
            no(msg, "tidak ada pengecualian")
        } catch (e: Throwable) {
            if (exceptionClass.isInstance(e)) ok(msg, e::class.simpleName.toString())
            else no(msg, "tipe ${e::class.simpleName}")
        }
    }

    // assertAll: kumpulkan beberapa assertion, jalankan semua, laporkan jika ada yg gagal
    fun assertAll(grup: String, blok: AssertAllScope.() -> Unit) {
        val scope = AssertAllScope()
        scope.blok()
        println("--- assertAll[$grup]: ${scope.count} assertion dijalankan, ${scope.failed} gagal ---")
        if (scope.failed == 0) pass++ else fail++
    }

    fun ringkasan() {
        println()
        println("Ringkasan: pass=$pass fail=$fail")
    }
}

class AssertAllScope {
    var count = 0
    var failed = 0
    fun <T> assertEquals(expected: T, actual: T, msg: String) {
        count++
        if (expected == actual) println("  [LULUS] $msg: $actual")
        else { println("  [GAGAL]  $msg: expected=$expected actual=$actual"); failed++ }
    }
}

fun main() {
    // assertEquals & assertNotEquals
    Assert.assertEquals(4, 2 + 2, "penjumlahan")
    Assert.assertNotEquals(5, 2 + 2, "bukan 5")

    // assertTrue
    Assert.assertTrue("Kotlin".startsWith("Kot"), "startsWith Kot")

    // assertNull & assertNotNull
    data class Pengguna(val nama: String)
    val nullPengguna: Pengguna? = null
    val budi: Pengguna? = Pengguna("Budi")
    Assert.assertNull(nullPengguna, "null reference")
    Assert.assertNotNull(budi, "non-null reference")

    // assertThrows
    Assert.assertThrows(IllegalArgumentException::class, {
        require(1 > 5) { "tidak mungkin" }
    }, "require melempar IAE")

    // assertAll: kelompok assertion dieksekusi semua, bukan berhenti di yg pertama gagal
    Assert.assertAll("validasi Pengguna Budi") {
        assertEquals("Budi", budi?.nama ?: "", "nama")
        assertEquals(4, "Budi".length, "panjang nama")
        assertEquals(true, budi != null, "tidak null")
    }

    Assert.ringkasan()
}

Output yang Dihasilkan:

text
[LULUS] penjumlahan — 4
[LULUS] bukan 5 — actual=4
[LULUS] startsWith Kot — true
[LULUS] null reference — null
[LULUS] non-null reference — value=Pengguna(nama=Budi)
[LULUS] require melempar IAE — IllegalArgumentException
  [LULUS] nama: Budi
  [LULUS] panjang nama: 4
  [LULUS] tidak null: true
--- assertAll[validasi Pengguna Budi]: 3 assertion dijalankan, 0 gagal ---

Ringkasan: pass=7 fail=0

Mari kita telaah beberapa bagian penting dari file di atas:

  1. Object Assert: meniru API assertion JUnit 5. Setiap method mencatat hasil lulus/gagal dan menambah penghitung. Pada produksi, JUnit menangkap AssertionFailedError dan menghentikan test.
  2. assertNull / assertNotNull: memeriksa referensi. Pattern umum: assertNotNull(x) diikuti penggunaan x dengan operator smart-cast non-null.
  3. assertThrows(exceptionClass, blok): menjalankan blok di dalam try/catch. Jika pengecualian cocok dengan kelas yang diharapkan, assertion lulus. Tanpa pengecualian, assertion gagal.
  4. assertAll(grup, blok): blok diberi scope AssertAllScope yang mengumpulkan semua hasil assertion. Berbeda dengan assertion biasa yang berhenti pada kegagalan pertama, assertAll menjalankan seluruh assertion dan melaporkan jumlah gagal.

assertAll vs Assertion Biasa #

Perbedaan penting antara assertion biasa dan assertAll terletak pada perilaku saat gagal. Assertion biasa melempar AssertionError seketika, sehingga assertion berikutnya tidak dijalankan. assertAll menampung kegagalan, melanjutkan assertion berikutnya, dan mengumpulkan semua hasil di akhir. Pola ini berguna ketika kita ingin melihat seluruh kegagalan sekaligus, bukan hanya yang pertama.

Pesan yang Membantu Debugging #

Setiap method assertion menerima parameter message opsional. Pesan ini muncul saat assertion gagal dan sangat membantu saat test dijalankan dalam CI. Tanpa pesan, output hanya menampilkan expected vs actual; dengan pesan, kita bisa menambahkan konteks seperti "gagal karena email belum dinormalisasi".

JUnit 5 mendukung dua bentuk pesan: string biasa yang selalu dievaluasi, dan lambda supplier yang hanya dievaluasi saat assertion gagal. Lambda supplier lebih efisien untuk pesan yang memerlukan komputasi — mis. assertEquals(expected, actual) { "Konteks user=${user.id} waktu=${clock.now()}" }. String tetap dievaluasi setiap kali assertion dipanggil, sementara lambda hanya dipanggil saat kegagalan terjadi.

Assertion dengan Timeout #

Beberapa assertion JUnit 5 menerima parameter timeout. assertTimeout(Duration.ofMillis(100)) { ... } gagal jika blok belum selesai dalam 100ms. assertTimeoutPreemptively menjalankan blok di thread terpisah dan membatalkannya saat timeout. Keduanya berguna untuk memverifikasi bahwa kode tidak menggantung, tetapi penggunaan timeout di unit test sering menandai test yang rapuh — lebih baik mock dependency lambat daripada mengandalkan timeout.

Assertion Kustom #

Untuk domain spesifik, kita dapat menulis assertion kustom yang membungkus JUnit 5. Mis. assertValidEmail(value) yang memanggil assertTrue(value.contains("@")) plus pemeriksaan domain. Assertion kustom meningkatkan keterbacaan dan mengurangi duplikasi. Taruh mereka di file Assertions.kt pada direktori testFixtures agar dapat dipakai lintas class test.

Referensi Lanjutan #

  • kotlin.test stdlib API — rujukan assertion kotlin.test seperti assertEquals, assertNotNull, dan assertContains
  • JUnit 5 user guide — panduan JUnit Jupiter untuk assertion bawaan dan mekanisme pelaporan kegagalan

Selanjutnya #

Setelah menguasai assertion, langkah berikutnya adalah menjalankan test yang sama dengan banyak input sekaligus — fitur @ParameterizedTest yang memungkinkan satu method menerima ratusan kasus uji.