Ketika satu method harus diuji dengan puluhan input berbeda, menulis satu @Test per kasus menjadi tidak praktis. Test menjadi berisik, duplikatif, dan sulit dipelihara. @ParameterizedTest dari JUnit 5 mengatasi masalah ini dengan menjalankan method yang sama berkali-kali, sekali untuk setiap baris input. Chapter ini memperkenalkan empat sumber data utama: @ValueSource (satu kolom), @CsvSource (multi kolom), dan @MethodSource (hasil function kompleks).
Mengapa Parameterized Test #
Bayangkan menulis 20 method @Test untuk menguji FizzBuzz pada angka 1 sampai 20. Setiap method memiliki struktur yang sama: panggil fizzbuzz(n), bandingkan dengan string yang diharapkan. Dengan @ParameterizedTest, kita tulis satu method yang menerima n dan expected, lalu JUnit menjalankannya 20 kali dengan input berbeda. Test suite tetap ringkas, dan menambahkan kasus baru cukup dengan menambah baris ke sumber data.
Empat sumber input utama:
| Sumber | Bentuk | Cocok untuk |
|---|---|---|
@ValueSource | Array tunggal (ints, strings, dll.) | Test satu input sederhana |
@CsvSource | Baris CSV multi kolom | Test dengan input + expected |
@MethodSource | Function yang mengembalikan Stream<Arguments> | Test dengan objek kompleks |
@EnumSource | Semua nilai enum | Test untuk setiap anggota enum |
Penulisan dengan @ParameterizedTest #
Bentuk asli JUnit 5 terlihat seperti berikut. Setiap baris pada sumber menjadi satu pemanggilan test terpisah dengan nama yang menyertakan input.
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.junit.jupiter.params.provider.ValueSource
import org.junit.jupiter.params.provider.MethodSource
import org.junit.jupiter.api.Assertions.assertEquals
class FizzBuzzTest {
private val fb = FizzBuzz()
@ParameterizedTest
@ValueSource(ints = [3, 6, 9, 12, 18])
fun `kelipatan 3 menghasilkan Fizz`(n: Int) {
assertEquals("Fizz", fb.hitung(n))
}
@ParameterizedTest
@CsvSource(
"1, 1",
"3, Fizz",
"5, Buzz",
"15, FizzBuzz",
"7, 7"
)
fun `fizzbuzz untuk berbagai input`(n: Int, expected: String) {
assertEquals(expected, fb.hitung(n))
}
}
class FizzBuzz {
fun hitung(n: Int): String = when {
n % 15 == 0 -> "FizzBuzz"
n % 3 == 0 -> "Fizz"
n % 5 == 0 -> "Buzz"
else -> n.toString()
}
}Untuk demonstrasi runnable tanpa runner JUnit, berikut simulasi yang menelusuri tiap sumber input dan menjalankan assertion untuk setiap baris:
// parameterized_demo.kt
// Mendemonstrasikan @ParameterizedTest, @ValueSource, @CsvSource,
// @MethodSource dari JUnit 5 dengan simulasi: kita iterasi sumber data
// dan menjalankan assertion untuk setiap baris.
class FizzBuzz {
fun hitung(n: Int): String = when {
n % 15 == 0 -> "FizzBuzz"
n % 3 == 0 -> "Fizz"
n % 5 == 0 -> "Buzz"
else -> n.toString()
}
}
object TestRunner {
private var pass = 0
private var fail = 0
fun <T> parameterized(namaGrup: String, sumber: List<T>, blok: (T) -> Unit) {
println("@ParameterizedTest[$namaGrup] — ${sumber.size} input")
for (input in sumber) {
try {
blok(input)
println(" [LULUS] input=$input")
pass++
} catch (e: AssertionError) {
println(" [GAGAL] input=$input — ${e.message}")
fail++
}
}
}
fun ringkasan() {
println()
println("Total: pass=$pass fail=$fail")
}
}
// Implementasi mini AssertionError agar pesan gagal tertangkap
class AssertionError(private val pesan: String) : kotlin.AssertionError(pesan) {
override val message: String get() = pesan
}
fun main() {
val fb = FizzBuzz()
// @ValueSource(Ints): satu kolom input sederhana
TestRunner.parameterized(
"ValueSource fizzbuzz-3",
listOf(3, 6, 9, 12, 18)
) { n ->
val hasil = fb.hitung(n)
if (hasil != "Fizz") throw AssertionError("expected=Fizz actual=$hasil")
}
// @CsvSource: dua kolom input + expected
data class CsvBaris(val input: Int, val expected: String)
val csv = listOf(
CsvBaris(1, "1"),
CsvBaris(3, "Fizz"),
CsvBaris(5, "Buzz"),
CsvBaris(15, "FizzBuzz"),
CsvBaris(7, "7")
)
TestRunner.parameterized("CsvSource fizzbuzz-lengkap", csv) { baris ->
val hasil = fb.hitung(baris.input)
if (hasil != baris.expected) throw AssertionError("expected=${baris.expected} actual=$hasil")
}
// @MethodSource: hasil dari function sebagai sumber data
val kasusTepi: List<Pair<Int, String>> = buildList {
add(0 to "FizzBuzz")
add(-3 to "Fizz")
add(30 to "FizzBuzz")
}
TestRunner.parameterized("MethodSource kasus-tepi", kasusTepi) { (n, expected) ->
val hasil = fb.hitung(n)
if (hasil != expected) throw AssertionError("expected=$expected actual=$hasil")
}
TestRunner.ringkasan()
}Output yang Dihasilkan:
@ParameterizedTest[ValueSource fizzbuzz-3] — 5 input
[LULUS] input=3
[LULUS] input=6
[LULUS] input=9
[LULUS] input=12
[LULUS] input=18
@ParameterizedTest[CsvSource fizzbuzz-lengkap] — 5 input
[LULUS] input=CsvBaris(input=1, expected=1)
[LULUS] input=CsvBaris(input=3, expected=Fizz)
[LULUS] input=CsvBaris(input=5, expected=Buzz)
[LULUS] input=CsvBaris(input=15, expected=FizzBuzz)
[LULUS] input=CsvBaris(input=7, expected=7)
@ParameterizedTest[MethodSource kasus-tepi] — 3 input
[LULUS] input=(0, FizzBuzz)
[LULUS] input=(-3, Fizz)
[LULUS] input=(30, FizzBuzz)
Total: pass=13 fail=0Mari kita bedah tiga sumber input yang berbeda pada file di atas:
@ValueSource: parametersumberadalahList<Int>sederhana. Untuk setiap elemen, kita panggil method test dengan satu argumen. Cocok untuk test yang hanya butuh satu input tanpa expected yang berbeda.@CsvSource: tiap baris punya dua kolom —inputdanexpected. Kita bungkus dalam data classCsvBarisagar terbaca. Pada produksi, JUnit otomatis memetakan kolom ke parameter method berdasarkan urutan.@MethodSource: sumber data adalah function yang mengembalikanList<Pair<Int, String>>. Pola ini cocok ketika input harus dibangun dengan logika (mis. kasus edge, objek kompleks, atau data dari file).
Output pada simulasi menampilkan jumlah input yang diproses dan jumlah kasus yang lulus — total 13 input, semua lulus. Pada JUnit nyata, setiap input muncul sebagai baris terpisah pada laporan HTML — jika satu kasus gagal, kita langsung tahu input mana yang bermasalah tanpa harus membedah test secara manual.
@MethodSource di JUnit 5 Asli #
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.api.Assertions.assertEquals
import java.util.stream.Stream
class EdgeCaseTest {
@ParameterizedTest
@MethodSource("kasusTepi")
fun `kasus tepi fizzbuzz`(n: Int, expected: String) {
assertEquals(expected, FizzBuzz().hitung(n))
}
companion object {
@JvmStatic
fun kasusTepi(): Stream<Arguments> = Stream.of(
Arguments.of(0, "FizzBuzz"),
Arguments.of(-3, "Fizz"),
Arguments.of(30, "FizzBuzz")
)
}
}@EnumSource untuk Coverage Enum #
Saat SUT bergantung pada enum, @EnumSource otomatis menghasilkan test untuk setiap anggota enum. Opsi mode = EnumSource.Mode.EXCLUDE memungkinkan kita mengecualikan nilai tertentu — mis. UNKNOWN yang seharusnya melempar pengecualian.
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource
import org.junit.jupiter.api.Assertions.assertNotNull
class StatusOrderTest {
@ParameterizedTest
@EnumSource(StatusOrder::class)
fun `setiap status memiliki deskripsi non-null`(status: StatusOrder) {
assertNotNull(status.deskripsi)
}
}
enum class StatusOrder(val deskripsi: String) {
PENDING("Menunggu pembayaran"),
PAID("Sudah dibayar"),
SHIPPED("Dikirim"),
DELIVERED("Diterima")
}Penamaan Parameterized Test #
JUnit 5 menghasilkan nama test default seperti [1] 3, Fizz yang kurang informatif. Anotasi @ParameterizedTest(name = "{0} menghasilkan {1}") mengganti pola nama sehingga laporan test membaca seperti kalimat — 3 menghasilkan Fizz, 5 menghasilkan Buzz, dst. Penamaan yang baik memudahkan debugging saat satu kasus gagal di antara puluhan lainnya.
Referensi Lanjutan #
- JUnit 5 user guide
— panduan resmi parameterized test JUnit 5 dengan
@ParameterizedTest,@ValueSource, dan@MethodSource
Selanjutnya #
Parameterized test menyebabkan satu method dipanggil berkali-kali. Pemahaman lifecycle — @BeforeEach yang dijalankan sebelum setiap pemanggilan — menjadi krusial. Chapter test-lifecycle akan membedah urutan eksekusi lengkap dari @BeforeAll sampai @AfterAll.