Setiap test JUnit 5 tidak berjalan dalam ruang hampa — ada rangkaian method lifecycle yang mengelilinginya. Memahami urutan ini penting karena banyak bug halus muncul dari asumsi yang salah tentang kapan sebuah setup dijalankan. Chapter ini membedah urutan @BeforeAll → @BeforeEach → @Test → @AfterEach → @AfterAll, serta perbedaan penting antara method static (default JUnit 5) dan instance method dengan Lifecycle.PER_CLASS.

Urutan Eksekusi #

Saat runner JUnit menjalankan sebuah class test, urutannya adalah:

  1. @BeforeAll — dijalankan satu kali untuk seluruh class, sebelum test pertama. Cocok untuk resource berat seperti koneksi database, container, atau file.
  2. Untuk setiap method @Test:
    • @BeforeEach — dijalankan sebelum isi test. Membuat instance baru, reset state, siapkan fixture.
    • @Test — method test itu sendiri.
    • @AfterEach — dijalankan setelah isi test selesai. Cleanup resource instance.
  3. @AfterAll — dijalankan satu kali setelah seluruh test selesai. Tutup koneksi, hapus container.

Urutan ini menjamin isolasi antar test: setiap test menerima state yang baru bersih, bukan sisa dari test sebelumnya.

Static vs Instance Method #

Secara default JUnit 5 membuat instance baru class test untuk setiap method @Test. Konsekuensinya, @BeforeAll dan @AfterAll harus static (Java) atau di companion object + @JvmStatic (Kotlin) — karena instance belum ada saat method dipanggil.

Alternatif: anotasi @TestInstance(Lifecycle.PER_CLASS) membuat instance dibuat sekali untuk seluruh class, sehingga @BeforeAll dan @AfterAll dapat berupa instance method biasa. Trade-off: state instance menetap antar test, sehingga @BeforeEach harus mereset dengan teliti.

kotlin Contoh: @BeforeAll dengan companion object
import org.junit.jupiter.api.*
import org.junit.jupiter.api.TestInstance.Lifecycle

class LifecycleTest {
    companion object {
        @BeforeAll
        @JvmStatic
        fun initSemua() {
            // resource berat sekali saja
        }
        @AfterAll
        @JvmStatic
        fun cleanupSemua() { }
    }

    @BeforeEach
    fun initPerTest() { }

    @AfterEach
    fun cleanupPerTest() { }

    @Test fun tesSatu() { }
    @Test fun tesDua() { }
}

@TestInstance(Lifecycle.PER_CLASS)
class LifecyclePerClassTest {
    @BeforeAll fun init() { }  // tidak perlu static
    @Test fun tesSatu() { }
}

Untuk demonstrasi runnable tanpa runner JUnit, berikut simulasi yang merekam urutan eksekusi lengkap:

kotlin
// lifecycle_demo.kt
// Mendemonstrasikan urutan eksekusi lifecycle JUnit 5:
// @BeforeAll (sekali) -> [@BeforeEach -> @Test -> @AfterEach] x N
// -> @AfterAll (sekali). Catatan: @BeforeAll/@AfterAll biasanya
// mensyaratkan method static atau @TestInstance(Lifecycle.PER_CLASS).

class LifecycleTracker {
    private val log = mutableListOf<String>()
    private var instanceCount = 0

    // Simulasi @BeforeAll: dijalankan sekali untuk seluruh kelas test
    fun beforeAll() {
        log += "[BeforeAll] inisialisasi resource berat (koneksi, container, dsb.)"
    }

    // Simulasi @AfterAll: dijalankan sekali setelah semua @Test selesai
    fun afterAll() {
        log += "[AfterAll] pembersihan resource berat"
    }

    // Simulasi @BeforeEach: tiap instance baru sebelum setiap @Test
    fun beforeEach() {
        instanceCount++
        log += "[BeforeEach] instance #$instanceCount dibuat"
    }

    // Simulasi @AfterEach: setelah setiap @Test
    fun afterEach() {
        log += "[AfterEach] instance #$instanceCount dibersihkan"
    }

    fun test(nama: String, blok: () -> Unit) {
        beforeEach()
        log += "  [Test] mulai: $nama"
        blok()
        log += "  [Test] selesai: $nama"
        afterEach()
    }

    fun cetakLog() {
        for (baris in log) println(baris)
    }
}

fun main() {
    val tracker = LifecycleTracker()

    // @BeforeAll dipanggil satu kali sebelum seluruh test
    tracker.beforeAll()

    // Tiga method @Test, masing-masing dikelilingi @BeforeEach/@AfterEach
    tracker.test("transaksi kecil") {
        // isi test (simulasi)
        println("    >> eksekusi transaksi kecil")
    }
    tracker.test("transaksi menengah") {
        println("    >> eksekusi transaksi menengah")
    }
    tracker.test("transaksi besar") {
        println("    >> eksekusi transaksi besar")
    }

    // @AfterAll dipanggil satu kali setelah seluruh test
    tracker.afterAll()

    println()
    println("=== Urutan eksekusi (log lengkap) ===")
    tracker.cetakLog()
}

Output yang Dihasilkan:

text
    >> eksekusi transaksi kecil
    >> eksekusi transaksi menengah
    >> eksekusi transaksi besar

=== Urutan eksekusi (log lengkap) ===
[BeforeAll] inisialisasi resource berat (koneksi, container, dsb.)
[BeforeEach] instance #1 dibuat
  [Test] mulai: transaksi kecil
  [Test] selesai: transaksi kecil
[AfterEach] instance #1 dibersihkan
[BeforeEach] instance #2 dibuat
  [Test] mulai: transaksi menengah
  [Test] selesai: transaksi menengah
[AfterEach] instance #2 dibersihkan
[BeforeEach] instance #3 dibuat
  [Test] mulai: transaksi besar
  [Test] selesai: transaksi besar
[AfterEach] instance #3 dibersihkan
[AfterAll] pembersihan resource berat

Mari kita amati log urutan yang dihasilkan:

  1. [BeforeAll] muncul satu kali di awal — sebelum test pertama dijalankan. Inilah tempat membuka koneksi database, memulai container Testcontainers, atau memuat data fixture berat.
  2. Untuk setiap test, blok [BeforeEach] → [Test] → [AfterEach] berulang. Instance counter bertambah di @BeforeEach, lalu dibersihkan di @AfterEach.
  3. [AfterAll] muncul satu kali di akhir — setelah seluruh test selesai. Inilah tempat menutup koneksi dan menghentikan container.
  4. Isolasi: setiap test menerima instance nomor yang berbeda (1, 2, 3). State tidak bocor antar test karena @BeforeEach selalu mereset.

Kapan Pakai PER_CLASS #

Lifecycle PER_CLASS dipakai ketika setup resource berat tidak boleh diulang antar test — mis. membuka koneksi sekali lalu membagikannya ke seluruh test. Konsekuensinya, kita harus teliti mereset state di @BeforeEach agar tidak ada kebocoran. Pola ini sering dipakai pada integration test yang berbagi container Testcontainers.

Urutan Eksekusi antar Method #

JUnit 5 secara default tidak menjamin urutan eksekusi antar method @Test — runner bebas memilih urutan apa pun. Untuk test yang bergantung pada urutan, anotasi @TestMethodOrder(MethodOrderer.OrderAnnotation.class) plus @Order(N) pada tiap method memaksa urutan eksplisit. Hindari pola ini kecuali benar-benar perlu — test yang bergantung pada urutan biasanya menandai desain yang rapuh.

kotlin Contoh: urutan eksplisit
import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestMethodOrder

@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class AlurTest {
    @Test @Order(1) fun `insert data`() { }
    @Test @Order(2) fun `update data`() { }
    @Test @Order(3) fun `delete data`() { }
}

Eksekusi Bersyarat #

Anotasi @EnabledIf dan @DisabledIf dari JUnit 5 memungkinkan test berjalan atau dilewati berdasarkan kondisi runtime — mis. sistem operasi, properti sistem, atau hasil evaluasi function. @EnabledOnOs(OS.LINUX) hanya menjalankan test di Linux. @EnabledIfSystemProperty(named = "env", matches = "ci") membatasi test hanya pada lingkungan CI. Pola ini menjaga test suite tetap bersih dari test yang tidak relevan dengan environment tertentu.

Referensi Lanjutan #

Selanjutnya #

Lifecycle mengatur kapan setup dijalankan, tetapi tidak mengatasi dependensi eksternal. Untuk mengisolasi SUT dari dependency nyata (database, API), kita perlu test doubles — konsep yang akan dibahas pada chapter mocking-fundamentals.