Anda menulis dua belas metamorphic relations untuk mesin pricing Anda. Setiap pengujian lulus. Anda merasa baik tentang coverage Anda.
Lalu seorang pelanggan melaporkan bahwa diskon bulk dihitung terbalik. Anda memeriksa suite relation Anda. Tidak satu pun pengujian yang gagal. Anda memiliki relation untuk konsistensi aditif, monotonicity, dan idempotence. Tidak satu pun dari mereka menangkap error tanda dalam pengali diskon.
Ini adalah rahasia kotor metamorphic testing: memiliki relation tidak sama dengan memiliki relation yang berguna. Sebuah metamorphic relation yang lemah lebih buruk daripada tidak ada pengujian sama sekali, karena relation tersebut meyakinkan Anda bahwa kode Anda benar padahal tidak.
Apa yang membuat sebuah relation “baik”?
Sebuah metamorphic relation yang baik memiliki kemampuan deteksi kegagalan yang tinggi. Relation tersebut menangkap bug nyata yang sebenarnya ditulis oleh programmer. Sisanya hanyalah overhead.
Contoh klasik adalah menguji function mean dengan bug off-by-one:
def buggy_mean(values):
"""Compute the arithmetic mean."""
return sum(values) / (len(values) - 1) # bug: off-by-one in denominator
Ini terlihat masuk akal jika Anda terbiasa dengan formula sample variance. Ini juga salah. Berikut empat relation yang umum ditulis orang untuk function mean, dan apa yang ditangkap masing-masing:
-
Boundedness: mean berada di antara min dan max. Lemah. Mean yang buggy masih memenuhi ini untuk sebagian besar input.
-
Idempotence pada konstanta:
mean([c] * n) == c. Sedang. Relation tersebut menangkap bug untuk daftar konstan, tetapi data acak jarang memicu kegagalan. -
Translation invariance:
mean([x + c for x in values]) == mean(values) + c. Kuat. Denominator yang buggy merusak ini untuk hampir setiap input yang tidak kosong. -
Scaling:
mean([x * k for x in values]) == mean(values) * k. Kuat. Alasan yang sama. Off-by-one bertahan penskalaan dalam tepat nol kasus yang menarik.
Jika test suite Anda hanya memeriksa boundedness dan idempotence konstan, off-by-one akan berlayar ke produksi. Anda akan memiliki metamorphic tests. Anda tidak akan memiliki deteksi bug.
Relation kuat vs. relation lemah
Perbedaan antara relation kuat dan relation lemah bukan seberapa cerdas relation tersebut terdengar. Relation tersebut adalah berapa banyak kelas kesalahan yang dieliminasi.
Sebuah relation lemah memeriksa properti yang kebanyakan implementasi salah secara kebetulan sudah memenuhi. Boundedness adalah contoh sempurna. Kebanyakan bug aritmatika mempertahankan boundedness karena penjumlahan dan perkalian tidak secara spontan menciptakan nilai di luar rentang input. Sebuah relation yang lulus untuk kode yang rusak adalah teater.
Sebuah relation kuat menyandikan constraint struktural yang dilanggar oleh implementasi yang rusak. Translation invariance kuat karena relation tersebut mengikat transformasi input ke transformasi output melalui kesetaraan yang tepat. Tidak ada ruang gerak.
Anda bisa mengukur ini secara formal. Dalam riset metamorphic testing, subsumption relation berarti bahwa relation A mendeteksi setiap kesalahan yang relation B deteksi, ditambah beberapa. Jika A mensubsumsi B, maka B redundan. Anda harus menyimpan A dan menghapus B.
Dalam praktik, Anda tidak membutuhkan bukti formal. Anda membutuhkan intuisi: jika sebuah relation masih akan lulus setelah Anda dengan sengaja memperkenalkan bug yang masuk akal, relation tersebut lemah. Buang saja.
Relation yang baik mencakup domain kesalahan yang berbeda
Satu relation kuat tidak cukup. Sebuah relation menangkap satu kelas kesalahan. Program nyata berisi banyak tipe bug independen, dan set relation Anda perlu mencakup mereka.
Pertimbangkan sebuah function pengurutan. Berikut relation yang diurutkan berdasarkan apa yang ditangkap:
Permutation: output berisi elemen yang persis sama dengan input. Menangkap bug drop/duplikat. Melewatkan bug pengurutan.
Order: output tidak menurun. Menangkap bug perbandingan. Melewatkan bug permutation.
Idempotence: sort(sort(x)) == sort(x). Hanya menangkap implementasi yang benar-benar rusak yang merusak sortedness. Hampir tidak berguna.
Stability: jika Anda memasangkan setiap elemen dengan index aslinya, key yang sama tetap dalam urutan input. Menangkap operator perbandingan yang menggunakan >= alih-alih >.
Substructure: mengurutkan prefix lalu full list harus setuju pada urutan prefix. Menangkap bug early-termination.
Sebuah test suite dengan hanya permutation dan idempotence akan melewatkan sort yang selalu mengembalikan [1, 2, 3]. Sebuah suite dengan permutation dan order menangkap bug itu. Tambahkan stability dan Anda menangkap sort yang tidak stabil juga.
Poinnya bukan mengumpulkan sebanyak mungkin relation. Poinnya adalah mencakup mode kegagalan yang independen. Dua relation yang menangkap bug yang sama lebih buruk daripada satu relation yang menangkap bug yang berbeda.
Trade-off: relation yang lebih kuat lebih sulit ditemukan
Ada alasan tim menulis relation lemah. Relation kuat memerlukan pengetahuan domain. Anda perlu memahami struktur matematis dari masalah Anda dengan cukup baik untuk menyandikan invariant yang tidak jelas.
Untuk function mean, translation invariance jelas bagi siapa pun dengan latar belakang statistik. Untuk simulasi partikel, relation yang setara mungkin memerlukan pengetahuan bahwa dinamika Hamiltonian mempertahankan volume phase-space. Tidak setiap tim memiliki keahlian itu di tangan.
Biaya lainnya adalah debugging. Ketika relation kuat gagal, pelanggaran memberi tahu Anda bahwa beberapa properti struktural rusak, tetapi bug bisa berada di mana saja dalam rantai penalaran yang mengarah ke properti tersebut. Sebuah relation lemah seperti “panjang output sama dengan panjang input” gagal dengan tepat satu cara. Sebuah relation kuat seperti “transformasi Fourier dari sinyal yang digeser memperoleh suku fase linear” gagal dalam seratus cara, dan melacak mana yang merupakan bug Anda membutuhkan waktu lebih lama.
Ini adalah ketegangan sentral. Relation lemah mudah ditulis, mudah di-debug, dan sebagian besar tidak berguna. Relation kuat sulit ditulis, sulit di-debug, dan benar-benar menemukan bug. Tidak ada makan siang gratis.
Cara mengevaluasi metamorphic relation
Sebelum menambahkan relation ke test suite Anda, jalankan melalui tiga pemeriksaan:
Uji bug sengaja. Perkenalkan bug yang realistis dalam implementasi Anda. Apakah relation gagal? Jika tidak, relation tersebut tidak menarik bebannya sendiri. Coba error tanda, off-by-one, argumen yang tertukar, kondisi batas yang hilang. Ini adalah bug yang terjadi di produksi. Relation Anda harus menangkapnya.
Uji independensi. Lihat relation yang sudah ada. Apakah ada yang menangkap bug yang sama? Jika ya, relation baru ini redundan. Redundansi bukanlah keamanan. Redundansi adalah beban pemeliharaan tanpa manfaat marjinal.
Uji falsifiabilitas. Bisakah Anda membayangkan implementasi yang rusak yang masuk akal yang memenuhi relation tersebut? Jika Anda bisa membuat sketsa dalam tiga puluh detik, relation tersebut terlalu lemah. Sebuah relation yang baik harus terasa seperti constraint yang ketat, bukan saran yang samar.
Berikut tampilannya dalam kode untuk function mean:
import random
def mean(values):
return sum(values) / len(values)
def test_translation_invariance():
values = [random.uniform(-100, 100) for _ in range(20)]
c = 5.5
shifted = [x + c for x in values]
assert mean(shifted) == mean(values) + c
def test_scaling():
values = [random.uniform(-50, 50) for _ in range(20)]
k = 3.0
scaled = [x * k for x in values]
assert mean(scaled) == mean(values) * k
Sekarang perkenalkan bug off-by-one. Ubah len(values) menjadi len(values) - 1. Jalankan kedua pengujian. Translation invariance langsung gagal. Scaling langsung gagal. Boundedness mungkin akan lulus.
Itulah perbedaan antara relation yang layak mendapat tempat dalam suite Anda dan relation yang hanya memenuhi baris.
Mulai dari kelas kesalahan, bukan properti
Kesalahan yang dilakukan sebagian besar tim adalah melakukan brainstorming properti terlebih dahulu. Mereka bertanya, “Apa invariant yang dimiliki function ini?” Itu menghasilkan relation lemah, karena invariant mudah dinyatakan dan sulit dilanggar.
Sebaliknya, mulai dari kelas kesalahan. Tanyakan, “Bug apa yang akan ditulis programmer yang lelah dalam function ini?” Lalu temukan relation yang menangkap bug-bug tersebut.
Untuk function jarak geometri, bug yang mungkin adalah error tanda, pencampuran unit, dan ketidakcocokan dimensi. Sebuah relation yang memeriksa jarak non-negatif menangkap error tanda. Sebuah relation yang memeriksa penskalaan di bawah transformasi koordinat menangkap pencampuran unit. Sebuah relation yang memeriksa ketidaksamaan segitiga menangkap ketidakcocokan dimensi.
Jika Anda tidak bisa menyebutkan bug yang ditangkap oleh sebuah relation, Anda tidak membutuhkan relation tersebut.
Relation adalah resource yang langka. Gunakan dengan bijak.
Metamorphic testing bukan tentang metric coverage. Ini tentang kepercayaan. Satu relation kuat yang menangkap bug nyata lebih berharga daripada dua puluh relation lemah yang lulus untuk kode yang rusak.
Audit metamorphic tests yang sudah ada. Perkenalkan bug. Lihat apa yang gagal. Hapus yang tidak gagal. Lalu tambahkan satu relation untuk setiap kelas kesalahan yang sebenarnya Anda khawatirkan. Itulah test suite yang layak dipertahankan.