Apa yang Sebenarnya Didapat dari Differential Testing
Anda bisa memercayai differential testing tanpa bukti formal, tetapi hanya jika Anda memahami persis di mana ia gagal.
Kelemahannya disebut common-mode failure. Ketika setiap implementasi dari sebuah spesifikasi membuat asumsi yang sama dan salah, mereka semua sepakat dan test harness Anda menganggapnya lulus. N-version programming tidak melindungi Anda dari spesifikasi yang buruk.
Differential testing bekerja dengan menjalankan beberapa implementasi independen dari spesifikasi yang sama dengan input yang sama. Jika output mereka tidak sepakat, setidaknya satu di antaranya buggy. Jika mereka sepakat, Anda sementara menganggapnya baik.
Ini powerful karena menghilangkan kebutuhan akan test oracle. Sebuah oracle adalah sumber kebenaran yang mengetahui jawaban yang benar untuk setiap input. Untuk sistem yang kompleks, oracle seringkali lebih sulit dibangun daripada sistem itu sendiri. Sebuah tax engine, simulasi fisika, atau protocol decoder dapat diuji untuk konsistensi jauh sebelum Anda dapat membuktikan apa yang seharusnya menjadi setiap outputnya.
Tapi keraguan itu penting. Kesepakatan hanya membuktikan konsistensi. Ia tidak membuktikan kebenaran.
Mengapa Kesepakatan Bukan Berarti Kebenaran
Mode kegagalan yang semua orang pelajari secara teori tetapi lupa dalam praktik adalah common-mode fault. Ketika kesalahan berasal dari spesifikasi itu sendiri, atau dari asumsi bersama yang dibuat secara independen oleh setiap tim implementasi, setiap versi menghasilkan jawaban yang sama dan salah.
Spesifikasinya tidak harus salah secara jelas. Ia hanya perlu ambigu pada edge case yang otak manusia selesaikan dengan cara yang sama.
Bayangkan sebuah spesifikasi untuk fungsi yang menghitung area dari sebuah simple polygon dari daftar vertex. Spesifikasi tersebut menyediakan rumus shoelace. Ia tidak pernah menyebutkan urutan vertex.
Tiga tim mengimplementasikannya. Ketiganya mengasumsikan urutan counter-clockwise karena itu adalah cara diagram contoh digambar. Input clockwise menghasilkan area negatif dalam rumus mentah. Ketiganya diam-diam membungkus hasilnya dengan abs() karena area harus positif. Mereka sepakat pada setiap test case.
Tapi spesifikasi tidak pernah mengatakan clockwise tidak valid. Implementasinya konsisten dan salah karena kelalaian. Differential testing memberi lampu hijau pada semuanya.
Tiga Implementasi, Satu Spesifikasi Ambigu
Berikut adalah contoh konkret yang dapat Anda jalankan. Spesifikasinya berbunyi: “Parse sebuah duration string dan kembalikan total jumlah detik. Sebuah duration terdiri dari satu atau lebih komponen. Setiap komponen adalah bilangan bulat positif diikuti oleh huruf unit: h untuk jam, m untuk menit, s untuk detik.”
Tiga tim menerima spesifikasi ini dan menulis parser mereka sendiri.
import re
def parse_duration_a(s):
"""Team A: regex approach."""
if not isinstance(s, str):
raise TypeError("input must be a string")
m = re.fullmatch(r"(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?", s)
if not m or not any(m.groups()):
raise ValueError(f"invalid duration: {s}")
h, mn, sec = (int(x or 0) for x in m.groups())
return h * 3600 + mn * 60 + sec
def parse_duration_b(s):
"""Team B: left-to-right scanner."""
if not isinstance(s, str):
raise TypeError("input must be a string")
total = 0
i = 0
while i < len(s):
j = i
while j < len(s) and s[j].isdigit():
j += 1
if j == i:
raise ValueError(f"expected number at position {i}")
num = int(s[i:j])
if j >= len(s):
raise ValueError(f"missing unit after {num}")
unit = s[j]
if unit == 'h':
total += num * 3600
elif unit == 'm':
total += num * 60
elif unit == 's':
total += num
else:
raise ValueError(f"invalid unit: {unit}")
i = j + 1
return total
def parse_duration_c(s):
"""Team C: state machine with duplicate detection."""
if not isinstance(s, str):
raise TypeError("input must be a string")
total = 0
seen = set()
i = 0
while i < len(s):
j = i
while j < len(s) and s[j].isdigit():
j += 1
if j == i:
raise ValueError("expected number")
num = int(s[i:j])
if j >= len(s):
raise ValueError("missing unit")
unit = s[j]
if unit in seen:
raise ValueError(f"duplicate unit: {unit}")
seen.add(unit)
i = j + 1
if unit == 'h':
total += num * 3600
elif unit == 'm':
total += num * 60
elif unit == 's':
total += num
else:
raise ValueError(f"invalid unit: {unit}")
return total
Sekarang kita menjalankan sebuah differential test harness yang memberi input yang sama ke ketiganya dan menandai ketidaksepakatan.
def differential_test(implementations, inputs):
for case in inputs:
results = []
errors = []
for impl in implementations:
try:
results.append(impl(case))
except Exception as e:
errors.append(type(e).__name__)
if errors:
if len(errors) == len(implementations) and len(set(errors)) == 1:
print(f"ALL ERROR on {case!r}: {errors[0]}")
else:
print(f"MIXED on {case!r}: results={results}, errors={errors}")
else:
if len(set(results)) == 1:
print(f"AGREE on {case!r}: {results[0]}")
else:
print(f"DISAGREE on {case!r}: {results}")
IMPLS = [parse_duration_a, parse_duration_b, parse_duration_c]
CASES = [
"1h30m", # normal
"90m", # single unit
"30m1h", # out of order
"1h2h", # duplicate unit
"0h", # zero is not positive
"1.5h", # decimal
"1H", # wrong case
]
differential_test(IMPLS, CASES)
Menjalankan ini menghasilkan:
AGREE on '1h30m': 5400
AGREE on '90m': 5400
MIXED on '30m1h': results=[5400, 5400], errors=['ValueError']
MIXED on '1h2h': results=[10800], errors=['ValueError', 'ValueError']
AGREE on '0h': 0
ALL ERROR on '1.5h': ValueError
ALL ERROR on '1H': ValueError
Harness tersebut menangkap masalah nyata. Pada 30m1h, regex Team A menolak input out-of-order sementara Team B dan C menerimanya. Pada 1h2h, scanner Team B diam-diam menambahkan kedua jam sementara deteksi duplikat Team C memunculkan error. Ini adalah bug yang seharusnya ditemukan oleh differential testing.
Tapi perhatikan 0h. Spesifikasi mengatakan “bilangan bulat positif.” Nol bukan positif. Ketiga implementasi menerimanya karena tidak ada dari tim yang menulis validasi untuk persyaratan yang terdengar jelas tetapi tidak ditegakkan. Mereka sepakat, jadi test lulus. Ini adalah common-mode failure yang tersembunyi di depan mata.
Hal yang sama terjadi pada 1H. Ketiganya menolaknya karena spesifikasi menunjukkan unit huruf kecil. Tapi jika spesifikasi bermaksud case-insensitive matching, setiap implementasi salah dan mereka salah bersama.
Cara Membuat Differential Testing Lebih Sedikit Salah
Anda tidak dapat menghilangkan common-mode failure sepenuhnya tanpa bukti formal. Tapi Anda dapat membuatnya lebih tidak mungkin terjadi.
Diversifikasikan strategi implementasi, bukan hanya personel. Jika setiap tim menggunakan algoritma yang sama dari buku teks yang sama, Anda belum membangun diversitas. Anda telah membangun latency. Paksa satu tim menggunakan state machine, tim lain menggunakan parser generator, tim ketiga menggunakan rekursi. Algoritma yang berbeda gagal pada input yang berbeda.
Gunakan bahasa pemrograman yang berbeda. Bug shared standard library adalah common-mode failure yang klasik. Jika setiap implementasi menggunakan JSON parser atau floating-point math library yang sama, mereka berbagi bug-nya.
Tambahkan adversarial oracle. Beri tugas pada seseorang untuk menemukan input di mana spesifikasi ambigu. Tugas mereka adalah membuat implementasi tidak sepakat. Input yang memecah belah mereka adalah test paling berharga yang akan Anda tulis.
Fuzz secara agresif. Kesepakatan pada segelintir contoh yang dipilih secara manual adalah bukti yang lemah. Kesepakatan pada satu juta input yang dihasilkan secara acak lebih kuat. Fuzzing menemukan sudut-sudut dari ruang input yang tidak dipertimbangkan oleh tim manapun.
Test spesifikasinya sendiri. Tulis negative test eksplisit untuk persyaratan seperti “bilangan bulat positif” dan periksa bahwa setidaknya satu implementasi menolaknya. Jika ketiganya menerima input yang tidak valid, spesifikasi Anda perlu diperketat, bukan kode Anda.
Kapan Differential Testing Cukup
Differential testing bukan pengganti untuk formal verification. Ia adalah sebuah filter. Ia menangkap bug implementasi dengan murah dan dini, sebelum Anda berinvestasi dalam bukti.
Pertanyaannya bukan apakah Anda dapat memercayainya. Pertanyaannya adalah untuk apa Anda dapat memercayainya. Anda dapat memercayainya untuk menemukan ketidaksepakatan. Anda tidak dapat memercayainya untuk menemukan kesepakatan universal di hadapan spesifikasi yang buruk.
Jika Anda sedang membangun perangkat lunak safety-critical, differential testing adalah langkah awal. Jalankan, perbaiki ketidaksepakatannya, lalu ajukan spesifikasinya ke model checker atau proof assistant. Jika Anda sedang membangun web service, differential testing mungkin adalah semua kepercayaan yang Anda butuhkan untuk sebuah feature branch. Buktinya sebanding dengan taruhannya.
Jika Anda menjalankan N-version test suite hari ini, tambahkan satu test case lagi. Temukan input yang spesifikasi tidak definisikan dengan jelas. Jalankan melalui implementasi Anda. Jika mereka semua sepakat, Anda belum menemukan test yang bagus. Anda telah menemukan lubang spesifikasi.
Bug yang mematikan bukanlah yang mana implementasi Anda tidak sepakat. Bug itu adalah yang mana mereka sepakat karena alasan yang salah.
Pertanyaan yang Sering Diajukan
Apa itu differential testing?
Differential testing adalah teknik di mana beberapa implementasi independen dari spesifikasi yang sama dieksekusi dengan input yang identik. Output mereka dibandingkan. Ketidaksepakatan mengungkap bug tanpa memerlukan sumber kebenaran yang sudah ada untuk setiap input.
Apa itu common-mode failure dalam perangkat lunak?
Common-mode failure terjadi ketika beberapa komponen independen gagal pada input yang sama karena alasan dasar yang sama. Dalam N-version programming, ini biasanya terjadi ketika spesifikasi ambigu dan setiap tim menyelesaikan ambiguitas dengan cara yang sama.
Apa bedanya differential testing dengan property-based testing?
Property-based testing memeriksa bahwa output memenuhi aturan umum, seperti “sorting sebuah list tidak pernah mengubah panjangnya.” Differential testing memeriksa bahwa beberapa implementasi menghasilkan output yang sama. Kedua teknik saling melengkapi. Property-based testing menemukan logic error. Differential testing menemukan inkonsistensi.
Bisakah saya menggunakan LLM untuk menghasilkan implementasi yang beragam untuk differential testing?
Anda bisa, tapi hati-hati. Model yang dilatih pada corpus yang tumpang tindih cenderung menghasilkan kode dengan failure mode yang berkorelasi. Penelitian terbaru menunjukkan co-error rate antara 15% dan 30% untuk komponen yang dihasilkan AI. Jika Anda menggunakan LLM, mintalah algoritma dan bahasa yang benar-benar berbeda. Kumpulan rewrite yang semantiknya identik bukan diversitas.