Tujuh belas Chapter di belakang kita membangun tiap teknik pengujian satu per satu: assertion, lifecycle, fixture, mock, integration, coverage, sampai kode paralel. Chapter penutup ini merangkai semuanya menjadi satu disiplin: kebiasaan-kebiasaan kecil yang membedakan suite yang dipercaya seluruh tim dari suite yang diabaikan saat merah. Prinsipnya sederhana — test adalah dokumentasi yang bisa dijalankan, dan dokumentasi yang baik harus bisa dibaca tanpa membuka kode produksinya. Kita akan menutup Section ini dengan membandingkan dua gaya penulisan test secara langsung, lalu satu daftar periksa yang bisa Anda bawa ke setiap pull request.

Test sebagai Dokumentasi yang Berjalan #

Cara tercepat menilai suite: baca daftar namanya tanpa membuka isinya. Jika daftar tersebut terbaca seperti spesifikasi perilaku — setorMenambahSaldo, tarikMelebihiSaldoDitolak — maka suite itu layak dipercaya sebagai dokumentasi. Jika yang terbaca hanya test1, testSemua, atau ujiDompet, test tetap memverifikasi sesuatu tetapi tidak lagi berkomunikasi. Kebiasaan penamaan yang terbukti: perilaku_denganKondisi, misalnya tarikMelebihiSaldoDitolak atau diskonTidakBerlakuSaatStokHabis; tambahkan @DisplayName berbahasa manusia bila tim Anda lebih nyaman membaca laporan dalam kalimat utuh.

Manfaatnya kumulatif. Saat bug muncul, nama test mempersempit wilayah pencarian sebelum Anda membuka satu baris pun. Saat developer baru masuk, daftar test adalah tur berstruktur melalui perilaku sistem. Dan saat fitur berubah, test yang namanya tidak lagi sesuai perilaku langsung terlihat — sinyal bahwa dokumentasi perlu diperbarui.

Satu Aspek dan Pola AAA #

Satu method uji menguji satu aspek perilaku. Test yang memeriksa setor sekaligus tarik sekaligus penolakan punya tiga kemungkinan gagal dan satu nama — pembaca laporan tidak tahu aspek mana yang rusak. Pola arrange-act-assert menjaga fokus itu: data disiapkan, satu perilaku dipanggil, hasil diperiksa. Bila test Anda sulit dinamai karena memuat banyak aspek, pecah test itu; kesulitan memberi nama adalah sinyal desain, bukan kelemahan kosakata.

Tanpa Logika di Dalam Test #

Perulangan, percabangan, dan perhitungan di dalam test adalah kode yang menuntut pengujian sendiri — test yang butuh di-test tidak membantu siapa pun. Test sebaiknya berisi urutan pernyataan datar: bangun ini, panggil itu, bandingkan hasilnya. Bila beberapa variasi data memang perlu diuji, naikkan variasi itu ke parameterized test agar data dan pemeriksaannya terlihat sebagai tabel. Pengecualian yang wajar hanya satu: penyiapan objek kompleks, dan itupun lebih baik diangkat menjadi fixture atau object mother.

Determinisme dan Independensi Urutan #

Suite yang sehat bisa dijalankan: dari satu test saja, dari test mana pun lebih dahulu, di mesin mana pun, pada jam berapa pun. Tiga pengganggu determinisme sudah kita kenal sepanjang Section ini — waktu nyata (ganti dengan nilai beku seperti LocalDate.of(2026, 1, 15)), keacakan tanpa seed (ganti dengan new Random(42)), dan state sisa antar test (bersihkan lewat rollback atau penyiapan ulang). Test yang gagal secara acak lebih merusak daripada tidak adanya test, karena melatih tim mengabaikan warna merah.

Menjaga Suite Tetap Cepat #

Kecepatan suite menentukan seberapa sering suite dijalankan, dan seberapa sering dijalankan menentukan seberapa cepat bug ditemukan. Susun suite berlapis: unit test milidetik berjalan setiap kompilasi (bahkan otomatis di IDE saat menyimpan berkas), integration test detik-an berjalan sebelum merge, suite Testcontainers dan end-to-end berjalan di pipeline CI. Bila suite harian mulai terasa lambat, ukur: biasanya segelintir test lambat (tidur pakai sleep, koneksi nyata) menyeret seluruh suite — perbaiki mereka, bukan menaikkan timeout global.

Perbandingan Dua Gaya #

Program berikut menguji class Dompet yang sama dengan dua gaya. Gaya lama menumpuk tiga aspek dalam satu method bernama generik; gaya baik memecahnya menjadi tiga test bernama perilaku:

import java.util.List;

public class ContohPraktikUji {

    // ---- kode produksi yang diuji ----

    static final class Dompet {

        private long saldo;

        long saldo() {
            return saldo;
        }

        void setor(long jumlah) {
            if (jumlah <= 0) {
                throw new IllegalArgumentException("setoran harus positif");
            }
            saldo += jumlah;
        }

        void tarik(long jumlah) {
            if (jumlah > saldo) {
                throw new IllegalStateException("saldo tidak cukup");
            }
            saldo -= jumlah;
        }
    }

    private static void sama(Object diharapkan, Object aktual, String konteks) {
        if (!diharapkan.equals(aktual)) {
            throw new AssertionError(konteks + ": diharapkan "
                + diharapkan + " tetapi aktual " + aktual);
        }
    }

    // ---- gaya lama: satu method, banyak aspek, logika di dalam test ----

    static void gayaLamaSemuaSekaligus() {
        Dompet dompet = new Dompet();
        long[] setoran = {50_000, 30_000};       // data tanpa makna yang jelas
        for (long jumlah : setoran) {
            dompet.setor(jumlah);
        }
        sama(80_000L, dompet.saldo(), "setoran ganda");
        dompet.tarik(20_000);
        sama(60_000L, dompet.saldo(), "setelah tarik");
        boolean ditolak = false;
        try {
            dompet.tarik(999_999);
        } catch (IllegalStateException e) {
            ditolak = true;
        }
        if (!ditolak) {
            throw new AssertionError("tarik melebihi saldo seharusnya ditolak");
        }
    }

    // ---- gaya baik: satu aspek per test, nama menjelaskan perilaku ----

    static void dompetBaruBersaldoNol() {
        Dompet dompet = new Dompet();                     // arrange
        sama(0L, dompet.saldo(), "saldo awal");           // assert
    }

    static void setorMenambahSaldo() {
        Dompet dompet = new Dompet();                     // arrange
        dompet.setor(50_000L);                            // act
        sama(50_000L, dompet.saldo(), "setelah setor");   // assert
    }

    static void tarikMelebihiSaldoDitolak() {
        Dompet dompet = new Dompet();                     // arrange
        try {
            dompet.tarik(1L);                             // act
            throw new AssertionError("seharusnya ditolak");
        } catch (IllegalStateException e) {               // assert
            sama("saldo tidak cukup", e.getMessage(), "pesan penolakan");
        }
    }

    record Uji(String nama, Runnable blok) {}

    public static void main(String[] args) {
        List<Uji> suiteLama = List.of(
            new Uji("testSemuaSekaligus", ContohPraktikUji::gayaLamaSemuaSekaligus));

        List<Uji> suiteBaik = List.of(
            new Uji("dompetBaruBersaldoNol", ContohPraktikUji::dompetBaruBersaldoNol),
            new Uji("setorMenambahSaldo", ContohPraktikUji::setorMenambahSaldo),
            new Uji("tarikMelebihiSaldoDitolak", ContohPraktikUji::tarikMelebihiSaldoDitolak));

        System.out.println("== suite gaya lama ==");
        jalankan(suiteLama);
        System.out.println("== suite gaya baik ==");
        jalankan(suiteBaik);
    }

    static void jalankan(List<Uji> suite) {
        int lulus = 0;
        for (Uji uji : suite) {
            try {
                uji.blok().run();
                lulus++;
                System.out.println("LULUS  " + uji.nama());
            } catch (AssertionError e) {
                System.out.println("GAGAL  " + uji.nama() + " -> " + e.getMessage());
            }
        }
        System.out.println("ringkasan: " + lulus + "/" + suite.size() + " lulus");
    }
}

Output yang Dihasilkan:

text
== suite gaya lama ==
LULUS  testSemuaSekaligus
ringkasan: 1/1 lulus
== suite gaya baik ==
LULUS  dompetBaruBersaldoNol
LULUS  setorMenambahSaldo
LULUS  tarikMelebihiSaldoDitolak
ringkasan: 3/3 lulus

Bandingkan dua blok keluaran tersebut: suite lama hanya mengabarkan testSemuaSekaligus lulus; suite baik membacakan perilaku apa saja yang terbukti benar. Bedah baris-per-baris untuk bagian kuncinya:

  1. Baris long[] setoran = {50_000, 30_000};: data tanpa makna yang jelas — pembaca tidak tahu mengapa angka itu dipilih; pada gaya baik, tiap test hanya memuat angka yang relevan bagi satu perilaku.
  2. Baris for (long jumlah : setoran): logika perulangan di dalam test — kalau perulangan ini salah, test gagal karena test-nya sendiri cacat; ini kode yang menuntut pengujian tersendiri.
  3. Baris try { ... } catch (IllegalStateException e) { ditolak = true; }: pengecekan penolakan dengan flag manual tersebar di dua cabang; pada gaya baik, jalur gagal diuji langsung sebagai test tersendiri dengan pesan yang diverifikasi.
  4. Baris new Uji("dompetBaruBersaldoNol", ContohPraktikUji::dompetBaruBersaldoNol): nama test dan method-nya identik — nama adalah kontrak yang terbaca di laporan tanpa membuka isi.
  5. Baris jalankan(suiteBaik): penjalan yang sama untuk kedua suite; satu suite gagal tidak menghentikan suite lain — kebiasaan isolasi pada level eksekusi.

Daftar Periksa Penulis Test #

Rangkuman Section Testing sebagai daftar periksa yang bisa Anda jalankan pada setiap pull request:

PraktikTanda baikTanda perlu diperbaiki
PenamaanNama membaca seperti spesifikasitest1, ujiSemua
FokusSatu aspek per testBanyak aspek dalam satu method
StrukturArrange-act-assert terlihat jelasCampuran panggilan dan pemeriksaan
LogikaUrutan pernyataan datarLoop dan cabang di dalam test
DeterminismeNilai beku, seed tetap, rollbacknow(), random(), state sisa
IndependensiUrutan eksekusi bebasHanya lulus bila dijalankan berurutan
KecepatanMilidetik per unit testSleep, jaringan nyata di suite harian
VerifikasiAssertion tajam per aspekTest tanpa assertion sama sekali
DataNilai bercerita tentang skenarioData kembar yang tidak bisa dibedakan

Pengujian yang dikelola dengan disiplin ini berubah dari beban menjadi aset: setiap fitur baru datang dengan spesifikasi yang berjalan, setiap bug yang ditemukan menghasilkan test yang menjaganya agar tidak kembali, dan setiap refactoring dilakukan dengan jaring pengaman yang terbukti. Section berikutnya membawa kita ke wilayah yang mengambil alih sebagian disiplin ini secara otomatis: build tool, manajemen dependensi, dan pipeline otomatisasi.

Referensi #

Berkas Sumber

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

ContohPraktikUji.java

Unduh berkas
java
import java.util.List;

public class ContohPraktikUji {

    // ---- kode produksi yang diuji ----

    static final class Dompet {

        private long saldo;

        long saldo() {
            return saldo;
        }

        void setor(long jumlah) {
            if (jumlah <= 0) {
                throw new IllegalArgumentException("setoran harus positif");
            }
            saldo += jumlah;
        }

        void tarik(long jumlah) {
            if (jumlah > saldo) {
                throw new IllegalStateException("saldo tidak cukup");
            }
            saldo -= jumlah;
        }
    }

    private static void sama(Object diharapkan, Object aktual, String konteks) {
        if (!diharapkan.equals(aktual)) {
            throw new AssertionError(konteks + ": diharapkan "
                + diharapkan + " tetapi aktual " + aktual);
        }
    }

    // ---- gaya lama: satu method, banyak aspek, logika di dalam test ----

    static void gayaLamaSemuaSekaligus() {
        Dompet dompet = new Dompet();
        long[] setoran = {50_000, 30_000};       // data tanpa makna yang jelas
        for (long jumlah : setoran) {
            dompet.setor(jumlah);
        }
        sama(80_000L, dompet.saldo(), "setoran ganda");
        dompet.tarik(20_000);
        sama(60_000L, dompet.saldo(), "setelah tarik");
        boolean ditolak = false;
        try {
            dompet.tarik(999_999);
        } catch (IllegalStateException e) {
            ditolak = true;
        }
        if (!ditolak) {
            throw new AssertionError("tarik melebihi saldo seharusnya ditolak");
        }
    }

    // ---- gaya baik: satu aspek per test, nama menjelaskan perilaku ----

    static void dompetBaruBersaldoNol() {
        Dompet dompet = new Dompet();                     // arrange
        sama(0L, dompet.saldo(), "saldo awal");           // assert
    }

    static void setorMenambahSaldo() {
        Dompet dompet = new Dompet();                     // arrange
        dompet.setor(50_000L);                            // act
        sama(50_000L, dompet.saldo(), "setelah setor");   // assert
    }

    static void tarikMelebihiSaldoDitolak() {
        Dompet dompet = new Dompet();                     // arrange
        try {
            dompet.tarik(1L);                             // act
            throw new AssertionError("seharusnya ditolak");
        } catch (IllegalStateException e) {               // assert
            sama("saldo tidak cukup", e.getMessage(), "pesan penolakan");
        }
    }

    record Uji(String nama, Runnable blok) {}

    public static void main(String[] args) {
        List<Uji> suiteLama = List.of(
            new Uji("testSemuaSekaligus", ContohPraktikUji::gayaLamaSemuaSekaligus));

        List<Uji> suiteBaik = List.of(
            new Uji("dompetBaruBersaldoNol", ContohPraktikUji::dompetBaruBersaldoNol),
            new Uji("setorMenambahSaldo", ContohPraktikUji::setorMenambahSaldo),
            new Uji("tarikMelebihiSaldoDitolak", ContohPraktikUji::tarikMelebihiSaldoDitolak));

        System.out.println("== suite gaya lama ==");
        jalankan(suiteLama);
        System.out.println("== suite gaya baik ==");
        jalankan(suiteBaik);
    }

    static void jalankan(List<Uji> suite) {
        int lulus = 0;
        for (Uji uji : suite) {
            try {
                uji.blok().run();
                lulus++;
                System.out.println("LULUS  " + uji.nama());
            } catch (AssertionError e) {
                System.out.println("GAGAL  " + uji.nama() + " -> " + e.getMessage());
            }
        }
        System.out.println("ringkasan: " + lulus + "/" + suite.size() + " lulus");
    }
}