Ketika test berbagi objek yang sama berulang kali — pengguna default, pesanan dengan beberapa item, koneksi database — kode setup menumpuk di setiap method. Test fixtures menyederhanakan hal ini dengan menyediakan setup bersama yang dapat dipakai ulang. Chapter ini memperkenalkan tiga pola: @BeforeEach untuk instance setup yang dipakai setiap test, fixture function untuk membuat objek dengan nilai default yang dapat di-override, dan test data builder untuk menyusun objek kompleks langkah demi langkah.
Tiga Pola Fixture #
| Pola | Bentuk | Cocok untuk |
|---|---|---|
@BeforeEach | Method yang dijalankan runner sebelum tiap test | Setup instance umum (kalkulator, koneksi, fixture) |
| Fixture function | Fungsi dengan parameter default | Membuat objek dengan nilai default yang di-override |
| Test data builder | Class dengan method fluent | Menyusun objek kompleks dengan banyak field opsional |
Pilihan pola tergantung kompleksitas objek yang dibuat. Untuk objek datar dengan 2-3 field, fixture function cukup. Untuk objek dengan banyak field opsional yang sering dikombinasikan berbeda tiap test, builder memberikan fleksibilitas penuh.
@BeforeEach Shared Setup #
@BeforeEach menjalankan method sebelum setiap @Test. Pola ini cocok ketika banyak test membutuhkan instance awal yang sama — mis. kalkulator yang baru dibersihkan, list kosong, repository in-memory baru.
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class KalkulatorTest {
private lateinit var kalkulator: Kalkulator
@BeforeEach
fun setup() {
kalkulator = Kalkulator() // instance baru per test
}
@Test fun `tambah dua angka`() { }
@Test fun `bagi dengan nol melempar`() { }
}Fixture Function #
Fixture function menyediakan parameter default yang dapat di-override per test. Pola ini ringkas dan ekspresif — kita hanya menyebutkan field yang berbeda dari default.
// test_fixtures.kt
// Mendemonstrasikan shared setup: fixture function, test data builder,
// dan pola @BeforeEach untuk objek umum. Runnable tanpa dependency.
data class Pesanan(
val id: Int,
val pelanggan: String,
val item: List<BarisItem>,
val alamat: String,
val diskon: Int = 0
)
data class BarisItem(val sku: String, val nama: String, val qty: Int, val harga: Int)
// === Fixture function === membuat objek umum dengan nilai default.
fun pesananFixture(
id: Int = 1,
pelanggan: String = "Budi",
item: List<BarisItem> = listOf(BarisItem("B01", "Buku", 1, 10000)),
alamat: String = "Jl. Mawar No. 1",
diskon: Int = 0
): Pesanan = Pesanan(id, pelanggan, item, alamat, diskon)
// === Test Data Builder === objek yang akumulasikan parameter step by step.
class PesananBuilder {
private var id: Int = 1
private var pelanggan: String = "Budi"
private var item: MutableList<BarisItem> = mutableListOf()
private var alamat: String = "Jl. Mawar No. 1"
private var diskon: Int = 0
fun withId(id: Int) = apply { this.id = id }
fun withPelanggan(nama: String) = apply { this.pelanggan = nama }
fun tambahItem(item: BarisItem) = apply { this.item.add(item) }
fun withAlamat(alamat: String) = apply { this.alamat = alamat }
fun withDiskon(diskon: Int) = apply { this.diskon = diskon }
fun build(): Pesanan = Pesanan(id, pelanggan, item.toList(), alamat, diskon)
}
// === Pola @BeforeEach === fungsi yang dipanggil sebelum tiap test,
// mengembalikan instance baru.
class FixtureContext {
lateinit var pesananKosong: Pesanan
private set
lateinit var pesananSatu: Pesanan
private set
lateinit var pesananBesar: Pesanan
private set
fun beforeEach() {
pesananKosong = pesananFixture(item = emptyList(), pelanggan = "Tamu")
pesananSatu = pesananFixture(item = listOf(BarisItem("A1", "Apel", 2, 5000)))
pesananBesar = PesananBuilder()
.withId(99)
.withPelanggan("Siti")
.tambahItem(BarisItem("K1", "Kopi", 3, 15000))
.tambahItem(BarisItem("G1", "Gula", 1, 8000))
.withDiskon(10)
.build()
}
}
fun total(p: Pesanan): Int {
val subtotal = p.item.sumOf { it.qty * it.harga }
val diskon = subtotal * p.diskon / 100
return subtotal - diskon
}
fun main() {
// Simulasi @BeforeEach sebelum tiap test
val ctx = FixtureContext()
val tests = listOf(
"pesanan kosong total 0" to { check(total(ctx.pesananKosong) == 0) },
"pesanan satu item total 10000" to { check(total(ctx.pesananSatu) == 10000) },
"pesanan besar diskon 10%" to { check(total(ctx.pesananBesar) == 47700) }
)
for ((nama, blok) in tests) {
ctx.beforeEach() // shared setup
blok()
println("[LULUS] $nama")
}
// Demo Test Data Builder untuk skenario khusus
println()
val khusus = PesananBuilder()
.withId(777)
.withPelanggan("Dewi")
.tambahItem(BarisItem("T1", "Teh", 5, 3000))
.withAlamat("Jl. Anggrek No. 7")
.build()
println("Builder khusus: $khusus")
println("Total: ${total(khusus)}")
}Output yang Dihasilkan:
[LULUS] pesanan kosong total 0
[LULUS] pesanan satu item total 10000
[LULUS] pesanan besar diskon 10%
Builder khusus: Pesanan(id=777, pelanggan=Dewi, item=[BarisItem(sku=T1, nama=Teh, qty=5, harga=3000)], alamat=Jl. Anggrek No. 7, diskon=0)
Total: 15000Mari kita bedah tiga pola yang muncul pada file di atas:
pesananFixture(...): function dengan parameter default. Setiap test hanya menyebutkan field yang berbeda. Pola ini cocok untuk objek dengan beberapa field yang sering dipakai dengan nilai default sama.PesananBuilder: class dengan methodwithId,withPelanggan,tambahItem, dll. Setiap method mengembalikanthis(apply) sehingga dapat dirantaikan. Cocok untuk objek dengan banyak field opsional yang sering dikombinasikan berbeda tiap test.FixtureContext.beforeEach(): simulasi@BeforeEachyang mempersiapkan tiga instance pesanan berbeda (kosong, satu item, besar). Setiap test menerima instance baru sehingga tidak ada kebocoran state.
Test Data Builder #
Builder pattern memberi fleksibilitas penuh. Berikut perbandingan singkat dengan fixture function:
// Fixture function: ringkas untuk variasi kecil
val pesananA = pesananFixture(pelanggan = "Budi")
// Builder: fleksibel untuk objek kompleks
val pesananB = PesananBuilder()
.withId(777)
.withPelanggan("Siti")
.tambahItem(BarisItem("T1", "Teh", 5, 3000))
.tambahItem(BarisItem("K1", "Kopi", 2, 15000))
.withDiskon(15)
.build()Builder menghasilkan kode yang lebih panjang tetapi membaca seperti kalimat deskriptif. Fixture function lebih ringkas tetapi memaksa kita mengingat urutan parameter.
Anti-Pattern Fixture #
| Anti-pattern | Masalah |
|---|---|
Setup besar di @BeforeEach | Test jadi sulit dibaca — pembaca harus scroll untuk tahu state awal |
| Fixture function dengan 10 parameter | Sama rumitnya dengan constructor biasa; pakai builder |
| Builder tanpa default | Builder tidak berguna jika semua field wajib; tambah default |
| Shared state antar test via object | Test jadi non-deterministic — urutan eksekusi memengaruhi hasil |
Referensi Lanjutan #
- JUnit 5 user guide
— panduan resmi JUnit 5 untuk test fixture,
@BeforeEach, dan pola setup/teardown yang dapat dipakai ulang
Selanjutnya #
Setelah test terorganisir dengan fixture, langkah berikutnya adalah mengukur seberapa banyak kode yang benar-benar diuji. Chapter test-coverage akan memperkenalkan JaCoCo dan perbedaan line coverage dengan branch coverage.