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:

AnotasiTujuan
@TestMenandai method sebagai test; tidak boleh menerima argumen
@BeforeEachDijalankan sebelum setiap @Test; cocok untuk reset state
@AfterEachDijalankan setelah setiap @Test; cocok untuk cleanup
@DisplayNameMengganti nama test dengan teks deskriptif
@DisabledMenonaktifkan 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.

kotlin Contoh: class test JUnit 5
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:

kotlin
// 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:

text
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 stabil

Mari kita amati perilaku lifecycle dari output di atas:

  1. @BeforeEach dijalankan sebelum setiap @Test. Output menunjukkan [BeforeEach] kalkulator siap muncul sebelum isi test.
  2. @AfterEach dijalankan setelah isi @Test selesai. Counter panggilan bertambah per test, lalu dibersihkan pada blok AfterEach.
  3. @DisplayName menggantikan nama method dengan deskripsi yang lebih bermakna: "kuadrat(3) menghasilkan 9".
  4. @Disabled menyebabkan 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:

kotlin Contoh: build.gradle.kts JUnit 5
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 #

Selanjutnya #

Setelah memahami anotasi lifecycle, langkah berikutnya adalah menguasai assertion yang lebih lengkap — assertEquals, assertNull, assertThrows, hingga assertAll untuk pengelompokan.