Test yang kita tulis sejauh ini memilih contoh satu per satu: umur 13 masuk kelompok remaja, tarik 150_000 melempar exception. Contoh-contoh itu tepat tetapi terbatas — bug sering bersembunyi di kombinasi input yang tidak terpikirkan penulis test. Property-based testing membalik pendekatannya: alih-alih menyebut contoh, kita menyatakan properti — invarian yang harus berlaku untuk semua input yang sah — lalu framework menghasilkan ratusan hingga ribuan input acak untuk mencoba melanggar properti tersebut. Pada Chapter ini kita akan membedakan contoh dan properti, menulis properti berbasis Random ber-seed dari Java murni, mengenal jqwik sebagai framework standarnya di JVM, dan mempelajari heuristik menemukan properti pada kode kita sendiri.
Contoh Lawan Properti #
Test berbasis contoh menjawab “apakah benar untuk input ini?”. Test berbasis properti menjawab “apakah benar untuk input mana pun?”. Untuk fungsi Collections.sort, contoh menguji sort([3,1,2]) == [1,2,3]. Properti menguji hal-hal yang harus selalu benar: hasil selalu terurut menaik, jumlah elemen tidak berubah, tidak ada elemen baru muncul, dan tidak ada elemen lama hilang. Perhatikan bahwa properti tidak menyebut hasil akhir yang eksak — properti menyatakan hubungan antara masukan dan keluaran yang benar untuk tak hingga banyaknya daftar.
Kekuatan pendekatan ini muncul dari jumlahnya: satu properti dengan 1000 input acak sering menemukan kasus pinggir (daftar kosong, nilai kembar, angka negatif) yang tidak terlintas saat kita menulis contoh manual.
Anatomi Properti #
Sebuah properti terdiri dari tiga bagian: generator input yang menghasilkan nilai acak dalam domain sah, invarian yang diperiksa setelah kode dijalankan, dan kadang prasyarat yang membatasi input yang relevan. Framework jqwik menuliskannya langsung pada parameter method uji:
import net.jqwik.api.*;
import net.jqwik.api.constraints.Size;
class PropertiUrutanTest {
@Property
void hasilSelaluTerurutMenaik(@ForAll List<Integer> daftar) {
List<Integer> hasil = new ArrayList<>(daftar);
Collections.sort(hasil);
for (int i = 1; i < hasil.size(); i++) {
assertThat(hasil.get(i - 1)).isLessThanOrEqualTo(hasil.get(i));
}
}
@Property
void ukuranTidakBerubah(@ForAll @Size(max = 100) List<Integer> daftar) {
List<Integer> hasil = new ArrayList<>(daftar);
Collections.sort(hasil);
assertThat(hasil).hasSize(daftar.size());
}
}Anotasi @Property menandai method properti; @ForAll menghasilkan nilai acak untuk tiap parameter; @Size membatasi bentuk daftar. jqwik menjalankan ratusan iterasi per properti dan melaporkan input yang melanggar invarian. Prasyarat ditulis dengan Assumptions.that(kondisi).isTrue() — iterasi yang tidak memenuhi prasyarat diabaikan, tidak dihitung gagal.
Properti dari Java Murni #
Seluruh mekanismenya bisa kita bangun tanpa framework: generator dari Random ber-seed, invarian sebagai boolean, dan pelaporan jumlah pelanggaran:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
public class ContohProperti {
static long jumlahPemeriksaan = 0;
static long jumlahPelanggaran = 0;
static void cek(String namaProperti, boolean terpenuhi, String detail) {
jumlahPemeriksaan++;
if (!terpenuhi) {
jumlahPelanggaran++;
System.out.println("GAGAL " + namaProperti + " -> " + detail);
}
}
public static void main(String[] args) {
Random acak = new Random(42); // seed tetap: urutan angka deterministik
// properti 1: penjumlahan bersifat komutatif
for (int i = 0; i < 1_000; i++) {
long a = acak.nextInt(1_000_000) - 500_000;
long b = acak.nextInt(1_000_000) - 500_000;
cek("a + b == b + a", a + b == b + a, a + " + " + b);
}
// properti 2-4: pengurutan menjaga sifat daftar
for (int i = 0; i < 200; i++) {
int panjang = acak.nextInt(50);
List<Integer> daftar = new ArrayList<>();
for (int j = 0; j < panjang; j++) {
daftar.add(acak.nextInt(1000));
}
List<Integer> hasil = new ArrayList<>(daftar);
Collections.sort(hasil);
boolean menaik = true;
for (int j = 1; j < hasil.size(); j++) {
if (hasil.get(j - 1) > hasil.get(j)) {
menaik = false;
break;
}
}
cek("hasil terurut menaik", menaik, daftar.toString());
cek("ukuran daftar tetap", hasil.size() == daftar.size(), daftar.toString());
long totalAwal = 0;
for (int nilai : daftar) {
totalAwal += nilai;
}
long totalHasil = 0;
for (int nilai : hasil) {
totalHasil += nilai;
}
cek("total nilai tetap", totalAwal == totalHasil, daftar.toString());
}
System.out.println("pemeriksaan : " + jumlahPemeriksaan);
System.out.println("pelanggaran : " + jumlahPelanggaran);
}
}Output yang Dihasilkan:
pemeriksaan : 1600
pelanggaran : 0Enam belas ratus iterasi berlalu tanpa satu pelanggaran — dan hasilnya sama persis di setiap eksekusi. Bedah baris-per-baris bagian kuncinya:
- Baris
Random acak = new Random(42);: seed tetap membuat urutan angka acak dapat diprediksi; keluaran program deterministik dari eksekusi ke eksekusi — teknik yang sama dipakai jqwik lewat anotasi@Property(seed = 42)saat kegagalan harus direproduksi. - Baris
static void cek(String namaProperti, boolean terpenuhi, ...): satu pemeriksaan properti untuk satu input; kegagalan dicetak lengkap dengan input penyebabnya agar mudah dianalisis. - Baris
long a = acak.nextInt(1_000_000) - 500_000;: generator input — bilangan bulat pada rentang −500.000 hingga 499.999, mencakup negatif dan nol tanpa harus memikirkannya satu per satu. - Baris
cek("a + b == b + a", a + b == b + a, ...): invarian komutatif dinyatakan sebagai ekspresi boolean tunggal; 1.000 pasang input diuji dalam satu perulangan. - Baris
List<Integer> hasil = new ArrayList<>(daftar);: salinan dibuat sebelum pengurutan agar masukan asli tetap tersedia untuk perbandingan — properti selalu membandingkan keluaran dengan masukan yang sama. - Baris
cek("total nilai tetap", totalAwal == totalHasil, ...): invarian kekekalan — pengurutan boleh memindahkan posisi tetapi tidak boleh mengubah isi; pelanggaran invarian ini menandakan algoritme kehilangan atau menggandakan elemen.
Menemukan Properti pada Kode Sendiri #
Heuristik paling produktif untuk menemukan properti, diurutkan dari yang termudah diterapkan:
- Kekekalan (invarian): operasi boleh mengubah susunan tetapi tidak jumlah — sort menjaga ukuran dan total; transfer menjaga jumlah saldo total.
- Hukum aljabar: komutatif (
a + b == b + a), asosiatif ((a+b)+c == a+(b+c)), identitas (a + 0 == a) pada operasi domain seperti diskon, pajak, dan penggabungan. - Roundtrip:
urai(susun(x)) == xuntuk serialisasi, kompresi, dan encoding; jika bolak-balik mengembalikan nilai asli, kedua arah tersebut konsisten. - Oracle sederhana: implementasi lambat tapi pasti dibandingkan dengan implementasi cepat — misalnya penghitung mundur hasil rumus dibandingkan hasil perulangan.
- Idempotensi: menjalankan operasi dua kali memberikan hasil yang sama dengan sekali — penting untuk operasi pembersihan dan sinkronisasi.
Ketika sebuah properti gagal, jqwik menjalankan shrinking: framework mengecilkan input penyerang secara bertahap — dari [834, -21, 407] menjadi [2, 1] — sampai kasus minimal yang tetap melanggar. Kasus minimal inilah yang dilaporkan, sehingga diagnosis jauh lebih cepat daripada membaca satu angka acak panjang.
Property-based testing melengkapi — bukan menggantikan — test berbasis contoh: contoh menjaga kasus bisnis penting agar tetap eksplisit dan mudah dibaca, properti menjaga invarian struktural di lautan input. Berikutnya kita naik ke lapisan infrastruktur: Testcontainers, yang menghadirkan database sungguhan ke dalam test tanpa meninggalkan utang konfigurasi.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ContohProperti.java
Unduh berkasimport java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
public class ContohProperti {
static long jumlahPemeriksaan = 0;
static long jumlahPelanggaran = 0;
static void cek(String namaProperti, boolean terpenuhi, String detail) {
jumlahPemeriksaan++;
if (!terpenuhi) {
jumlahPelanggaran++;
System.out.println("GAGAL " + namaProperti + " -> " + detail);
}
}
public static void main(String[] args) {
Random acak = new Random(42); // seed tetap: urutan angka deterministik
// properti 1: penjumlahan bersifat komutatif
for (int i = 0; i < 1_000; i++) {
long a = acak.nextInt(1_000_000) - 500_000;
long b = acak.nextInt(1_000_000) - 500_000;
cek("a + b == b + a", a + b == b + a, a + " + " + b);
}
// properti 2-4: pengurutan menjaga sifat daftar
for (int i = 0; i < 200; i++) {
int panjang = acak.nextInt(50);
List<Integer> daftar = new ArrayList<>();
for (int j = 0; j < panjang; j++) {
daftar.add(acak.nextInt(1000));
}
List<Integer> hasil = new ArrayList<>(daftar);
Collections.sort(hasil);
boolean menaik = true;
for (int j = 1; j < hasil.size(); j++) {
if (hasil.get(j - 1) > hasil.get(j)) {
menaik = false;
break;
}
}
cek("hasil terurut menaik", menaik, daftar.toString());
cek("ukuran daftar tetap", hasil.size() == daftar.size(), daftar.toString());
long totalAwal = 0;
for (int nilai : daftar) {
totalAwal += nilai;
}
long totalHasil = 0;
for (int nilai : hasil) {
totalHasil += nilai;
}
cek("total nilai tetap", totalAwal == totalHasil, daftar.toString());
}
System.out.println("pemeriksaan : " + jumlahPemeriksaan);
System.out.println("pelanggaran : " + jumlahPelanggaran);
}
}