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:
@BeforeAll— dijalankan satu kali untuk seluruh class, sebelum test pertama. Cocok untuk resource berat seperti koneksi database, container, atau file.- 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.
@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.
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:
// 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:
>> 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 beratMari kita amati log urutan yang dihasilkan:
[BeforeAll]muncul satu kali di awal — sebelum test pertama dijalankan. Inilah tempat membuka koneksi database, memulai container Testcontainers, atau memuat data fixture berat.- Untuk setiap test, blok
[BeforeEach] → [Test] → [AfterEach]berulang. Instance counter bertambah di@BeforeEach, lalu dibersihkan di@AfterEach. [AfterAll]muncul satu kali di akhir — setelah seluruh test selesai. Inilah tempat menutup koneksi dan menghentikan container.- Isolasi: setiap test menerima instance nomor yang berbeda (1, 2, 3). State tidak bocor antar test karena
@BeforeEachselalu 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.
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 #
- JUnit 5 user guide
— panduan resmi lifecycle JUnit 5 mencakup
@BeforeEach,@AfterEach,@BeforeAll, dan@AfterAll - JUnit Jupiter API reference — rujukan API anotasi lifecycle JUnit Jupiter
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.