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 #
| Assertion | Tujuan |
|---|---|
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.
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:
// 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:
[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=0Mari kita telaah beberapa bagian penting dari file di atas:
- Object
Assert: meniru API assertion JUnit 5. Setiap method mencatat hasil lulus/gagal dan menambah penghitung. Pada produksi, JUnit menangkapAssertionFailedErrordan menghentikan test. assertNull/assertNotNull: memeriksa referensi. Pattern umum:assertNotNull(x)diikuti penggunaanxdengan operator smart-cast non-null.assertThrows(exceptionClass, blok): menjalankanblokdi dalamtry/catch. Jika pengecualian cocok dengan kelas yang diharapkan, assertion lulus. Tanpa pengecualian, assertion gagal.assertAll(grup, blok): blok diberi scopeAssertAllScopeyang mengumpulkan semua hasil assertion. Berbeda dengan assertion biasa yang berhenti pada kegagalan pertama,assertAllmenjalankan 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.testsepertiassertEquals,assertNotNull, danassertContains - 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.