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:
== suite gaya lama ==
LULUS testSemuaSekaligus
ringkasan: 1/1 lulus
== suite gaya baik ==
LULUS dompetBaruBersaldoNol
LULUS setorMenambahSaldo
LULUS tarikMelebihiSaldoDitolak
ringkasan: 3/3 lulusBandingkan 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:
- 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. - 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. - 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. - Baris
new Uji("dompetBaruBersaldoNol", ContohPraktikUji::dompetBaruBersaldoNol): nama test dan method-nya identik — nama adalah kontrak yang terbaca di laporan tanpa membuka isi. - 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:
| Praktik | Tanda baik | Tanda perlu diperbaiki |
|---|---|---|
| Penamaan | Nama membaca seperti spesifikasi | test1, ujiSemua |
| Fokus | Satu aspek per test | Banyak aspek dalam satu method |
| Struktur | Arrange-act-assert terlihat jelas | Campuran panggilan dan pemeriksaan |
| Logika | Urutan pernyataan datar | Loop dan cabang di dalam test |
| Determinisme | Nilai beku, seed tetap, rollback | now(), random(), state sisa |
| Independensi | Urutan eksekusi bebas | Hanya lulus bila dijalankan berurutan |
| Kecepatan | Milidetik per unit test | Sleep, jaringan nyata di suite harian |
| Verifikasi | Assertion tajam per aspek | Test tanpa assertion sama sekali |
| Data | Nilai bercerita tentang skenario | Data 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 berkasimport 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");
}
}