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:

SumberBentukCocok untuk
@ValueSourceArray tunggal (ints, strings, dll.)Test satu input sederhana
@CsvSourceBaris CSV multi kolomTest dengan input + expected
@MethodSourceFunction yang mengembalikan Stream<Arguments>Test dengan objek kompleks
@EnumSourceSemua nilai enumTest 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.

kotlin Contoh: @ParameterizedTest FizzBuzz
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:

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

text
@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=0

Mari kita bedah tiga sumber input yang berbeda pada file di atas:

  1. @ValueSource: parameter sumber adalah List<Int> sederhana. Untuk setiap elemen, kita panggil method test dengan satu argumen. Cocok untuk test yang hanya butuh satu input tanpa expected yang berbeda.
  2. @CsvSource: tiap baris punya dua kolom — input dan expected. Kita bungkus dalam data class CsvBaris agar terbaca. Pada produksi, JUnit otomatis memetakan kolom ke parameter method berdasarkan urutan.
  3. @MethodSource: sumber data adalah function yang mengembalikan List<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 #

kotlin Contoh: @MethodSource dengan Stream<Arguments>
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.

kotlin Contoh: @EnumSource
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.