JUnit 5 adalah framework test de-facto untuk JVM, dan Kotlin dapat memanfaatkannya sepenuhnya karena interoperabilitas penuh dengan Java. Bagian “Jupiter” adalah API pemrograman JUnit 5 yang berbeda dari JUnit 4 (Vintage). Chapter ini memperkenalkan anotasi utama Jupiter — @Test, @BeforeEach, @AfterEach, @DisplayName, dan @Disabled — serta menunjukkan urutan lifecycle saat test dijalankan oleh runner.
Anatomi JUnit 5 #
JUnit 5 terdiri dari tiga modul: JUnit Platform (runner), JUnit Jupiter (API untuk menulis test), dan JUnit Vintage (kompatibilitas JUnit 4). Untuk menulis test baru, kita fokus pada Jupiter yang menyediakan anotasi modern dan dukungan lambda untuk assertion.
Anotasi yang paling sering dipakai:
| Anotasi | Tujuan |
|---|---|
@Test | Menandai method sebagai test; tidak boleh menerima argumen |
@BeforeEach | Dijalankan sebelum setiap @Test; cocok untuk reset state |
@AfterEach | Dijalankan setelah setiap @Test; cocok untuk cleanup |
@DisplayName | Mengganti nama test dengan teks deskriptif |
@Disabled | Menonaktifkan test atau class; berguna saat test masih rusak |
Penulisan dengan JUnit 5 #
Bentuk asli test JUnit 5 pada file Kotlin terlihat seperti berikut. Karena butuh runner JUnit untuk eksekusi, contoh ini tidak dijalankan langsung oleh kotlinc; runner akan otomatis mendeteksi method @Test saat dijalankan via Gradle atau Maven.
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Assertions.assertEquals
class KalkulatorTest {
private lateinit var kalkulator: Kalkulator
@BeforeEach
fun setup() {
kalkulator = Kalkulator()
}
@AfterEach
fun teardown() {
// cleanup resource
}
@Test
@DisplayName("kuadrat(3) menghasilkan 9")
fun kuadratTiga() {
assertEquals(9, kalkulator.kuadrat(3))
}
@Test
@DisplayName("kuadrat(0) menghasilkan 0")
fun kuadratNol() {
assertEquals(0, kalkulator.kuadrat(0))
}
@Disabled("performa belum stabil")
@Test
@DisplayName("kuadrat bilangan besar")
fun kuadratBesar() {
assertEquals(1_000_000, kalkulator.kuadrat(1000))
}
}Untuk demonstrasi runnable, berikut simulasi yang merekam urutan eksekusi lifecycle yang sama persis seperti yang dilakukan runner JUnit:
// junit_jupiter_demo.kt
// Mendemonstrasikan perilaku anotasi JUnit 5 (@Test, @BeforeEach,
// @AfterEach, @DisplayName, @Disabled) dengan simulasi berbasis main().
// Simulasi registrar JUnit. Setiap method didaftarkan lalu dipanggil
// sesuai urutan lifecycle: BeforeEach -> Test -> AfterEach.
class JunitSimulator {
private val beforeEach: MutableList<() -> Unit> = mutableListOf()
private val afterEach: MutableList<() -> Unit> = mutableListOf()
private val tests: MutableList<Pair<String, () -> Unit>> = mutableListOf()
private val disabled: MutableList<Pair<String, String>> = mutableListOf()
fun beforeEach(blok: () -> Unit) { beforeEach += blok }
fun afterEach(blok: () -> Unit) { afterEach += blok }
fun test(displayName: String, blok: () -> Unit) { tests += displayName to blok }
fun disabled(displayName: String, alasan: String) { disabled += displayName to alasan }
fun jalankan() {
println("JUnit Jupiter: ${tests.size} aktif, ${disabled.size} disabled")
println()
for ((nama, blok) in tests) {
println("[TEST] $nama")
beforeEach.forEach { it() }
blok()
afterEach.forEach { it() }
println()
}
for ((nama, alasan) in disabled) {
println("[DISABLED] $nama — $alasan")
}
}
}
class Kalkulator {
var jumlahPanggilan: Int = 0
private set
fun kuadrat(n: Int): Int {
jumlahPanggilan++
return n * n
}
}
fun main() {
val sim = JunitSimulator()
val kalkulator = Kalkulator()
// @BeforeEach: reset state sebelum setiap @Test
sim.beforeEach {
// simulasi field injection & reset
kalkulator.hashCode() // noop, sekadar menjaga signature
println(" [BeforeEach] kalkulator siap")
}
// @AfterEach: cleanup resource setelah setiap @Test
sim.afterEach {
println(" [AfterEach] panggilan=${kalkulator.jumlahPanggilan}")
}
// @DisplayName diganti menjadi parameter deskriptif
sim.test("kuadrat(3) menghasilkan 9") {
val hasil = kalkulator.kuadrat(3)
check(hasil == 9) { "Expected 9, got $hasil" }
println(" [Assert] 9 == $hasil")
}
sim.test("kuadrat(0) menghasilkan 0") {
val hasil = kalkulator.kuadrat(0)
check(hasil == 0) { "Expected 0, got $hasil" }
println(" [Assert] 0 == $hasil")
}
// @Disabled: dilewati runner, dilaporkan tapi tidak dieksekusi
sim.disabled("kuadrat bilangan besar", "performa belum stabil")
sim.jalankan()
}Output yang Dihasilkan:
JUnit Jupiter: 2 aktif, 1 disabled
[TEST] kuadrat(3) menghasilkan 9
[BeforeEach] kalkulator siap
[Assert] 9 == 9
[AfterEach] panggilan=1
[TEST] kuadrat(0) menghasilkan 0
[BeforeEach] kalkulator siap
[Assert] 0 == 0
[AfterEach] panggilan=2
[DISABLED] kuadrat bilangan besar — performa belum stabilMari kita amati perilaku lifecycle dari output di atas:
@BeforeEachdijalankan sebelum setiap@Test. Output menunjukkan[BeforeEach] kalkulator siapmuncul sebelum isi test.@AfterEachdijalankan setelah isi@Testselesai. Counterpanggilanbertambah per test, lalu dibersihkan pada blok AfterEach.@DisplayNamemenggantikan nama method dengan deskripsi yang lebih bermakna:"kuadrat(3) menghasilkan 9".@Disabledmenyebabkan test dilewati runner tetapi dilaporkan — pada output muncul sebagai[DISABLED] kuadrat bilangan besar — performa belum stabil.
Pola @DisplayName sangat berguna pada test suite besar yang punya puluhan method @Test. Tanpa @DisplayName, laporan test hanya menampilkan nama method seperti kuadratTiga() — sulit dipahami tanpa membuka kode. Dengan @DisplayName, laporan membaca seperti daftar skenario: “kuadrat(3) menghasilkan 9”, “kuadrat(0) menghasilkan 0”, dan seterusnya. Tim dapat cepat memahami apa saja yang diuji tanpa harus membaca implementasi test.
Assertion Bawaan Jupiter #
JUnit 5 menyediakan method assertion di paket org.junit.jupiter.api.Assertions. Method utama yang sering dipakai: assertEquals(expected, actual), assertTrue(condition), assertThrows(ExceptionClass, executable), dan assertAll(executables...) untuk grup assertion yang semuanya dijalankan walaupun ada yang gagal.
Pesan kegagalan pada assertion Jupiter mendukung lambda supplier yang dievaluasi secara lazy — pesan hanya dibuat saat assertion gagal, bukan setiap kali assertion dipanggil. Pola assertEquals(expected, actual) { "konteks: nilai=$actual" } lebih efisien daripada string concatenation yang selalu dieksekusi. Untuk assertion kompleks seperti membandingkan koleksi, JUnit 5 menyediakan assertIterableEquals yang memeriksa elemen satu per satu.
Konfigurasi Gradle untuk JUnit 5 #
Untuk memakai JUnit 5 pada project Gradle Kotlin DSL, tambahkan plugin junit-platform dan dependensi junit-jupiter:
plugins {
kotlin("jvm") version "2.0.21"
id("org.jetbrains.kotlin.plugin.junit5") version "2.0.21"
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testImplementation("org.junit.platform:junit-platform-launcher:1.10.2")
}
tasks.test {
useJUnitPlatform()
}Referensi Lanjutan #
- JUnit 5 user guide
— panduan resmi JUnit 5 yang merinci
@Test,@BeforeEach, parameterized test, danTestInfo - JUnit Jupiter API reference
— rujukan API
org.junit.jupiter.apiuntuk anotasi dan assertion di lingkungan JVM
Selanjutnya #
Setelah memahami anotasi lifecycle, langkah berikutnya adalah menguasai assertion yang lebih lengkap — assertEquals, assertNull, assertThrows, hingga assertAll untuk pengelompokan.