Setiap test membutuhkan data awal — pelanggan, pesanan, rekening — dan kualitas data itu menentukan seberapa cepat suite dapat dipahami. Ketika data uji dibangun dengan constructor ber-argumen panjang yang disalin ke mana-mana, perubahan satu field memaksa kita menyentuh puluhan test; ketika data uji dipusatkan dengan rapi, perubahan hanya menyentuh satu tempat. Kumpulan data dan objek siap-pakai yang menyiapkan kondisi awal test disebut test fixture. Pada Chapter ini kita akan mengenali masalah constructor panjang, membangun data uji lewat pola builder, memusatkan skenario bernama lewat object mother, lalu menjaga fixture agar tetap ramping seiring bertambahnya jumlah test.

Apa itu Test Fixture #

Istilah fixture berasal dari dunia pengujian perangkat keras: perangkat penjepit yang menahan benda uji pada posisi tetap. Dalam pengujian perangkat lunak, fixture adalah kondisi awal yang tetap dan terkendali yang dipasang sebelum test berjalan — objek domain, data di tabel, berkas di disk, atau kumpulan konstanta. Fixture yang baik punya dua sifat: stabil (nilai sama setiap saat sehingga test deterministik) dan ekspresif (pembaca langsung mengerti skenario apa yang sedang disiapkan).

Fixture di JUnit biasanya dibangun di @BeforeEach atau di awal method test. Di mana pun ia dibangun, masalahnya sama: bagaimana menyusun data yang jelas niatnya tanpa menulis constructor ber-argumen panjang berulang-ulang.

Masalah Constructor Panjang #

Domain object nyata sering punya banyak field. Membangunnya langsung lewat constructor menghasilkan baris seperti ini:

java
Pelanggan siti = new Pelanggan(
    "Siti", "[email protected]", "Surabaya", 28,
    Tingkat.EMAS, 1200, true, false);

Baris tersebut sulit dibaca karena makna setiap posisi bergantung urutan parameter — angka 28 dan 1200 tidak menjelaskan diri sendiri. Lebih buruk lagi, test yang hanya butuh field tingkat tetap harus mengisi seluruh argumen dengan nilai apa pun yang valid. Ketika constructor bertambah satu field baru, seluruh pemanggilan di seluruh suite harus disunting; itulah biaya perawatan yang sebenarnya.

Builder untuk Data Uji #

Pola builder menyembunyikan nilai default yang wajar dan hanya meminta test mengisi field yang relevan untuk skenarionya:

java
Pelanggan premium = new PelangganBuilder()
    .nama("Siti")
    .tingkat(Tingkat.EMAS)
    .poin(1200)
    .build();

Tiga hal terjadi sekaligus: nilai default mengisi field lain, nama method menjelaskan setiap nilai, dan test tetap ringkas saat domain bertambah field baru. Versi utuhnya berikut berisi record domain, builder dengan default, dan object mother — semuanya Java murni:

import java.util.List;

public class ContohFixture {

    enum Tingkat { BIASA, PERAK, EMAS }

    static final class Pelanggan {
        private final String nama;
        private final String email;
        private final String kota;
        private final Tingkat tingkat;
        private final int poin;

        private Pelanggan(Builder b) {
            this.nama = b.nama;
            this.email = b.email;
            this.kota = b.kota;
            this.tingkat = b.tingkat;
            this.poin = b.poin;
        }

        String nama() { return nama; }
        String email() { return email; }
        String kota() { return kota; }
        Tingkat tingkat() { return tingkat; }
        int poin() { return poin; }

        @Override
        public String toString() {
            return String.format("%-6s %-22s %-8s %-6s %5d",
                nama, email, kota, tingkat, poin);
        }
    }

    // ---- builder: default wajar, ubah hanya yang relevan untuk test ----

    static final class Builder {
        private String nama = "Budi";
        private String email = "[email protected]";
        private String kota = "Bandung";
        private Tingkat tingkat = Tingkat.BIASA;
        private int poin = 0;

        Builder nama(String nilai) { this.nama = nilai; return this; }
        Builder email(String nilai) { this.email = nilai; return this; }
        Builder kota(String nilai) { this.kota = nilai; return this; }
        Builder tingkat(Tingkat nilai) { this.tingkat = nilai; return this; }
        Builder poin(int nilai) { this.poin = nilai; return this; }

        Pelanggan build() { return new Pelanggan(this); }
    }

    // ---- object mother: skenario bernama yang dipakai lintas test ----

    static final class DataContoh {
        static Pelanggan pelangganStandar() {
            return new Builder().build();
        }

        static Pelanggan pelangganPremium() {
            return new Builder()
                .nama("Siti").email("[email protected]")
                .tingkat(Tingkat.EMAS).poin(1200)
                .build();
        }

        static Pelanggan pelangganBaru() {
            return new Builder()
                .nama("Dewi").email("[email protected]").kota("Depok")
                .build();
        }
    }

    public static void main(String[] args) {
        List<Pelanggan> fixture = List.of(
            DataContoh.pelangganStandar(),
            DataContoh.pelangganPremium(),
            DataContoh.pelangganBaru(),
            new Builder().poin(75).build()   // variasi lokal: hanya poin diubah
        );

        System.out.printf("%-6s %-22s %-8s %-6s %5s%n", "nama", "email", "kota", "tingkat", "poin");
        for (Pelanggan p : fixture) {
            System.out.println(p);
        }
    }
}

Output yang Dihasilkan:

text
nama   email                  kota     tingkat  poin
Budi   [email protected]       Bandung  BIASA      0
Siti   [email protected]       Bandung  EMAS    1200
Dewi   [email protected]       Depok    BIASA      0
Budi   [email protected]       Bandung  BIASA     75

Empat baris data tercetak dari empat cara membuat fixture yang berbeda tingkat kenyamanannya. Bedah bagian-bagian kuncinya:

  1. Baris private Pelanggan(Builder b): constructor class Pelanggan dibuat private dan hanya menerima builder — semua pembuatan data lewat jalur terkontrol sehingga default tidak bisa terlewat.
  2. Baris private String nama = "Budi"; (dan kawan-kawannya): nilai default yang wajar tinggal di builder; test yang tidak peduli kota tidak perlu menyebutkannya.
  3. Baris Builder nama(String nilai) { ...; return this; }: setiap method pengisi mengembalikan this sehingga pemanggilan dapat dirangkai menjadi kalimat yang terbaca dari atas ke bawah.
  4. Baris static Pelanggan pelangganPremium(): object mother — pabrik skenario bernama; nama method menjadi dokumentasi skenario yang dipakai lintas test.
  5. Baris new Builder().poin(75).build(): variasi lokal di test; hanya field yang relevan disebut, sisanya mewarisi default — perubahan default otomatis diperbarui ke seluruh test.
  6. Baris String.format("%-6s %-22s ..."): laporan rapi memperlihatkan hasil akhir tiap fixture; keluaran ini sepenuhnya deterministik karena seluruh nilai bersifat tetap.

Object Mother untuk Skenario Bernama #

Builder menjawab “bagaimana menyusun data”, sedangkan object mother menjawab “di mana menyimpan skenario yang dipakai bersama”. Object mother adalah class berisi static factory untuk skenario bernama: pelangganPremium(), pesananMenungguPembayaran(), rekeningTerblokir(). Test yang membutuhkan pelanggan premium cukup memanggil pabriknya — tanpa peduli pelanggan premium terdiri dari field apa. Ketika definisi “premium” berubah (misalnya poin minimum naik dari 1000 ke 1200), hanya object mother yang disunting dan seluruh test mengikuti definisi baru secara serentak.

Kombinasi keduanya membentuk pola berlapis: object mother memanggil builder di dalamnya, test memanggil object mother untuk skenario umum dan builder langsung untuk variasi lokal. Aturan praktisnya: sekali sebuah susunan builder disalin ke tiga test, pindahkan susunan itu ke object mother.

Merawat Fixture agar Tetap Sehat #

Fixture yang dibiarkan tumbuh tanpa kendali berubah menjadi masalah baru: satu method raksasa yang menyiapkan dua puluh objek untuk seluruh class, yang pembacanya tidak tahu mana yang benar-benar dipakai test tertentu. Jaga fixture tetap ramping dengan tiga kebiasaan. Pertama, siapkan data seminimal mungkin untuk satu test — data yang tidak dipakai adalah derau. Kedua, pilih nilai yang saling berbeda mencolok ("Budi" lawan "Siti", poin 0 lawan 1200) agar kegagalan tidak tertutup oleh data yang kembar. Ketiga, hindari nilai yang terlihat seperti punya makna padahal tidak — string "test" untuk semua nama membuat assertion gagal sulit dibedakan.

Dengan data uji yang rapi, kita siap menghadapi tantangan berbeda: dependensi yang tidak bisa (dan tidak boleh) dihadirkan di unit test. Chapter berikutnya membahas fondasi mocking — memutus dependensi eksternal demi isolasi.

Referensi #

Berkas Sumber

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

ContohFixture.java

Unduh berkas
java
import java.util.List;

public class ContohFixture {

    enum Tingkat { BIASA, PERAK, EMAS }

    static final class Pelanggan {
        private final String nama;
        private final String email;
        private final String kota;
        private final Tingkat tingkat;
        private final int poin;

        private Pelanggan(Builder b) {
            this.nama = b.nama;
            this.email = b.email;
            this.kota = b.kota;
            this.tingkat = b.tingkat;
            this.poin = b.poin;
        }

        String nama() { return nama; }
        String email() { return email; }
        String kota() { return kota; }
        Tingkat tingkat() { return tingkat; }
        int poin() { return poin; }

        @Override
        public String toString() {
            return String.format("%-6s %-22s %-8s %-6s %5d",
                nama, email, kota, tingkat, poin);
        }
    }

    // ---- builder: default wajar, ubah hanya yang relevan untuk test ----

    static final class Builder {
        private String nama = "Budi";
        private String email = "[email protected]";
        private String kota = "Bandung";
        private Tingkat tingkat = Tingkat.BIASA;
        private int poin = 0;

        Builder nama(String nilai) { this.nama = nilai; return this; }
        Builder email(String nilai) { this.email = nilai; return this; }
        Builder kota(String nilai) { this.kota = nilai; return this; }
        Builder tingkat(Tingkat nilai) { this.tingkat = nilai; return this; }
        Builder poin(int nilai) { this.poin = nilai; return this; }

        Pelanggan build() { return new Pelanggan(this); }
    }

    // ---- object mother: skenario bernama yang dipakai lintas test ----

    static final class DataContoh {
        static Pelanggan pelangganStandar() {
            return new Builder().build();
        }

        static Pelanggan pelangganPremium() {
            return new Builder()
                .nama("Siti").email("[email protected]")
                .tingkat(Tingkat.EMAS).poin(1200)
                .build();
        }

        static Pelanggan pelangganBaru() {
            return new Builder()
                .nama("Dewi").email("[email protected]").kota("Depok")
                .build();
        }
    }

    public static void main(String[] args) {
        List<Pelanggan> fixture = List.of(
            DataContoh.pelangganStandar(),
            DataContoh.pelangganPremium(),
            DataContoh.pelangganBaru(),
            new Builder().poin(75).build()   // variasi lokal: hanya poin diubah
        );

        System.out.printf("%-6s %-22s %-8s %-6s %5s%n", "nama", "email", "kota", "tingkat", "poin");
        for (Pelanggan p : fixture) {
            System.out.println(p);
        }
    }
}