Bayangkan sebuah method yang mengelompokkan umur ke dalam empat kategori dengan tiga batas nilai. Menguji satu kombinasi per method uji berarti menulis delapan method yang isinya identik — hanya datanya yang berbeda. Parameterized test menghilangkan duplikasi tersebut: satu method uji menerima data dari sumber eksternal, dan runner menjalankannya sekali per baris data dengan laporan terpisah untuk setiap kombinasi. Pada Chapter ini kita akan mengenal sumber-sumber data JUnit Jupiter — @ValueSource, @EnumSource, @CsvSource, dan @MethodSource — lalu membangun tabel kasus versi Java murni untuk membuktikan bahwa parameterized test hanyalah perulangan yang dilaporkan per kasus.

Satu Perilaku Banyak Data #

Parameterized test cocok ketika satu perilaku harus berlaku konsisten pada banyak kombinasi masukan-keluaran. Method uji ditulis sekali dengan parameter, dan data uji dipisahkan dari logika pemeriksaan. Manfaatnya bukan sekadar hemat baris: saat satu kasus gagal, laporan menyebut kombinasi data mana yang bermasalah tanpa menghentikan kasus lain — sesuatu yang tidak diberikan assertion berulang di dalam satu method biasa.

Sumber Data Sederhana #

Anotasi @ValueSource memasok satu nilai per pemanggilan untuk parameter tunggal — cocok untuk perilaku yang berlaku seragam pada banyak masukan:

java
@ParameterizedTest
@ValueSource(ints = {0, 12, 60, 120})
void umurValidTidakDitolak(int umur) {
    assertDoesNotThrow(() -> KelompokUmur.kelompok(umur));
}

@ParameterizedTest
@ValueSource(strings = {"Budi", "Siti", "Dewi"})
void namaPendaftarDiterima(String nama) {
    assertThat(Validator.namaSah(nama)).isTrue();
}

Anotasi @EnumSource mengambil seluruh konstanta enum — atau sebagian lewat atribut names — sehingga menambah konstanta baru otomatis menambah cakupan uji:

java
@ParameterizedTest
@EnumSource(Tingkat.class)
void setiapTingkatMemilikiKelasMalam(Tingkat tingkat) {
    assertThat(Jadwal.kelasMalam(tingkat)).isNotEmpty();
}

Kasus Multi Kolom dengan @CsvSource #

Ketika tiap kasus memuat pasangan masukan-harapan, @CsvSource menyusunnya sebagai baris comma-separated yang terbaca seperti tabel:

java
@ParameterizedTest
@CsvSource({
    "0,   anak",
    "12,  anak",
    "13,  remaja",
    "19,  remaja",
    "20,  dewasa",
    "59,  dewasa",
    "60,  lansia"
})
void batasKelompokUmurTepat(int umur, String harapan) {
    assertEquals(harapan, KelompokUmur.kelompok(umur),
        "umur " + umur + " masuk kelompok " + harapan);
}

Nilai kosong ditulis sebagai string kosong "" di kolom terkait, dan null ditulis sebagai teks null. Format CSV cocok hingga sekitar dua sampai empat kolom; lebih dari itu barisnya mulai sulit dibaca dan @MethodSource menjadi pilihan yang lebih terawat.

Data Kompleks dengan @MethodSource #

@MethodSource mengambil data dari sebuah static method yang mengembalikan Stream, Iterable, atau array — tempat kita bebas membangun objek kompleks, membaca konstanta, atau menghitung turunan data:

java
static Stream<Arguments> tabelUmur() {
    return Stream.of(
        Arguments.of(0, "anak"),
        Arguments.of(13, "remaja"),
        Arguments.of(59, "dewasa"),
        Arguments.of(60, "lansia")
    );
}

@ParameterizedTest
@MethodSource("tabelUmur")
void kelompokSesuaiTabel(int umur, String harapan) {
    assertEquals(harapan, KelompokUmur.kelompok(umur));
}

Static factory Arguments.of(...) membentuk satu baris kasus; nama method sumber dirujuk sebagai string pada atribut @MethodSource. Bila method sumber berada di class lain, rujuk dengan nama lengkapnya com.example.UjiData#tabelUmur.

Tabel Kasus dari Java Murni #

Mekanisme parameterized test dapat kita buktikan tanpa framework: sebuah List berisi record kasus, satu perulangan yang menjalankan perilaku untuk tiap baris, dan laporan per kasus. Tabel berikut sengaja memuat satu baris ber-harapan usang (umur 13 masih dianggap anak) supaya kita melihat bagaimana satu kasus gagal tanpa menyembunyikan kasus lain:

import java.util.List;

public class ContohTabelKasus {

    static String kelompokUmur(int umur) {
        if (umur < 0) {
            throw new IllegalArgumentException("umur tidak boleh negatif: " + umur);
        }
        if (umur <= 12) {
            return "anak";
        }
        if (umur <= 19) {
            return "remaja";
        }
        if (umur <= 59) {
            return "dewasa";
        }
        return "lansia";
    }

    record Kasus(int umur, String harapan) {}

    static final List<Kasus> TABEL = List.of(
        new Kasus(0, "anak"),
        new Kasus(12, "anak"),
        new Kasus(13, "anak"),      // harapan usang: batas remaja 13
        new Kasus(19, "remaja"),
        new Kasus(20, "dewasa"),
        new Kasus(59, "dewasa"),
        new Kasus(60, "lansia"),
        new Kasus(120, "lansia")
    );

    public static void main(String[] args) {
        System.out.printf("%-6s %-10s %-10s %-7s%n", "umur", "harapan", "aktual", "hasil");
        int lulus = 0;
        int gagal = 0;
        for (Kasus kasus : TABEL) {
            String aktual = kelompokUmur(kasus.umur());
            boolean cocok = kasus.harapan().equals(aktual);
            if (cocok) {
                lulus++;
            } else {
                gagal++;
            }
            System.out.printf("%-6d %-10s %-10s %-7s%n",
                kasus.umur(), kasus.harapan(), aktual, cocok ? "LULUS" : "GAGAL");
        }
        System.out.println("ringkasan: " + lulus + " lulus, " + gagal + " gagal");
    }
}

Output yang Dihasilkan:

text
umur   harapan    aktual     hasil  
0      anak       anak       LULUS  
12     anak       anak       LULUS  
13     anak       remaja     GAGAL  
19     remaja     remaja     LULUS  
20     dewasa     dewasa     LULUS  
59     dewasa     dewasa     LULUS  
60     lansia     lansia     LULUS  
120    lansia     lansia     LULUS  
ringkasan: 7 lulus, 1 gagal

Setiap baris data menjadi satu baris laporan — persis seperti bagaimana IntelliJ IDEA atau Maven menampilkan parameterized test yang gagal di satu kombinasi. Bedah baris-per-baris untuk bagian intinya:

  1. Baris record Kasus(int umur, String harapan) {}: satu baris tabel sebagai nilai yang tidak bisa diubah; kedua kolom (masukan dan harapan) menyatu dalam satu tipe.
  2. Baris static final List<Kasus> TABEL = List.of(...): sumber data — padanan @MethodSource; urutan List.of tetap sehingga laporan deterministik dari eksekusi ke eksekusi.
  3. Baris new Kasus(13, "anak"): kasus dengan harapan usang; baris ini sengaja salah untuk memperlihatkan pelaporan per kasus, bukan per method.
  4. Baris for (Kasus kasus : TABEL): runner menjalankan method produksi sekali per kasus dan membandingkan hasil aktual dengan harapan secara terpisah.
  5. Baris System.out.printf("%-6d %-10s ...: laporan tabel berkolom masukan-harapan-aktual-status, meniru tampilan laporan parameterized test di IDE.
  6. Baris hitungan lulus/gagal: ringkasan akhir menghitung kasus, bukan method — satuan pelaporan bergeser dari “satu method uji” menjadi “satu kombinasi data”.

Kekuatan parameterized test terletak pada kualitas tabelnya. Dua heuristik utama: equivalence partitioning membagi rentang masukan menjadi kelompok yang diperlakukan sama oleh kode (misalnya umur 0-12), lalu mengambil satu wakil per kelompok; boundary value analysis menambahkan nilai tepat di batas kelompok (12, 13, 19, 20) karena kekeliruan operator < versus <= hampir selalu bersembunyi di sana. Tambahkan pula kasus tepi seperti masukan kosong, nol, dan nilai terbesar yang masuk akal untuk domainnya.

Dengan tabel kasus yang lengkap, lapisan berikutnya pada pemahaman kita adalah siklus hidup method uji: kode penyiapan yang berjalan sebelum tiap test dan pembersihan sesudahnya. Chapter berikutnya membedah urutan @BeforeEach hingga @AfterAll secara eksperimen.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

ContohTabelKasus.java

Unduh berkas
java
import java.util.List;

public class ContohTabelKasus {

    static String kelompokUmur(int umur) {
        if (umur < 0) {
            throw new IllegalArgumentException("umur tidak boleh negatif: " + umur);
        }
        if (umur <= 12) {
            return "anak";
        }
        if (umur <= 19) {
            return "remaja";
        }
        if (umur <= 59) {
            return "dewasa";
        }
        return "lansia";
    }

    record Kasus(int umur, String harapan) {}

    static final List<Kasus> TABEL = List.of(
        new Kasus(0, "anak"),
        new Kasus(12, "anak"),
        new Kasus(13, "anak"),      // harapan usang: batas remaja 13
        new Kasus(19, "remaja"),
        new Kasus(20, "dewasa"),
        new Kasus(59, "dewasa"),
        new Kasus(60, "lansia"),
        new Kasus(120, "lansia")
    );

    public static void main(String[] args) {
        System.out.printf("%-6s %-10s %-10s %-7s%n", "umur", "harapan", "aktual", "hasil");
        int lulus = 0;
        int gagal = 0;
        for (Kasus kasus : TABEL) {
            String aktual = kelompokUmur(kasus.umur());
            boolean cocok = kasus.harapan().equals(aktual);
            if (cocok) {
                lulus++;
            } else {
                gagal++;
            }
            System.out.printf("%-6d %-10s %-10s %-7s%n",
                kasus.umur(), kasus.harapan(), aktual, cocok ? "LULUS" : "GAGAL");
        }
        System.out.println("ringkasan: " + lulus + " lulus, " + gagal + " gagal");
    }
}