Metamorphic testing telah menemukan 147 bug yang dikonfirmasi di GCC dan LLVM, cacat di simulator ADAS komersial yang digunakan oleh OEM otomotif, dan kesalahan fatal dalam sistem persepsi mobil self-driving delapan hari sebelum mobil tersebut menabrak pejalan kaki. Teknik ini terdengar akademis, tetapi bug-bugnya bukan.
Masalahnya adalah oracle problem. Bagi banyak program, Anda bisa menjalankan input tetapi tidak bisa memverifikasi secara independen apakah outputnya benar. Berapa jalur terpendek eksak melalui jaringan jalan dengan 10.000 node? Apakah optimasi compiler ini mempertahankan semantik? Apakah klasifikasi model ML ini benar-benar tepat? Anda tidak tahu. Unit testing tradisional gagal di sini karena Anda tidak bisa menulis assertEquals(expected, actual) ketika Anda tidak tahu apa seharusnya expected.
Metamorphic testing menghindari ini dengan sama sekali tidak memeriksa output. Ia memeriksa hubungan antar-output.
Apa itu metamorphic testing?
Metamorphic testing adalah teknik di mana Anda mengubah sebuah input menjadi input yang terkait, menjalankan keduanya melalui program Anda, dan menegaskan bahwa dua output tersebut mematuhi hubungan matematis atau logis yang diketahui. Hubungan tersebut disebut metamorphic relation.
Jika program Anda menghitung rata-rata dari daftar angka, Anda tidak perlu mengetahui rata-rata eksak dari [4.2, 1.7, 9.3, 2.1] untuk mengujinya. Anda hanya perlu tahu bahwa mengacak daftar tersebut harus menghasilkan hasil yang sama, atau menggandakan setiap elemen harus menggandakan rata-ratanya. Ini adalah metamorphic relations.
Input pertama adalah source test case. Input yang diubah adalah follow-up test case. Oraclenya adalah hubungan itu sendiri.
Berikut adalah contoh konkret dalam Python:
import random
def compute_average(numbers):
"""Returns the arithmetic mean of a list of numbers."""
if not numbers:
raise ValueError("empty list")
return sum(numbers) / len(numbers)
def test_average_permutation_invariant():
"""MR-1: Shuffling the input should not change the average."""
source = [4.2, 1.7, 9.3, 2.1, 5.6]
follow_up = source.copy()
random.shuffle(follow_up)
source_out = compute_average(source)
follow_up_out = compute_average(follow_up)
assert source_out == follow_up_out, (
f"Permutation MR failed: {source_out} != {follow_up_out}"
)
def test_average_scaling():
"""MR-2: Doubling every element should double the average."""
source = [3.0, 6.0, 9.0]
follow_up = [x * 2 for x in source]
source_out = compute_average(source)
follow_up_out = compute_average(follow_up)
assert follow_up_out == source_out * 2, (
f"Scaling MR failed: {follow_up_out} != {source_out * 2}"
)
def test_average_inclusion():
"""MR-3: Appending the average itself should not decrease the average."""
source = [10.0, 20.0, 30.0]
source_out = compute_average(source)
follow_up = source + [source_out]
follow_up_out = compute_average(follow_up)
assert follow_up_out == source_out, (
f"Inclusion MR failed: {follow_up_out} != {source_out}"
)
if __name__ == "__main__":
test_average_permutation_invariant()
test_average_scaling()
test_average_inclusion()
print("All metamorphic relations passed.")
Jika salah satu dari hubungan ini gagal, Anda telah menemukan bug tanpa pernah menghitung rata-rata yang diharapkan secara manual. Ini adalah ide intinya.
Bug nyata yang ditemukan dalam sistem produksi
Teknik ini tidak bersifat teoritis. Berikut adalah kasus-kasus yang didokumentasikan di mana metamorphic testing menemukan bug nyata dalam perangkat lunak produksi.
147 bug di GCC dan LLVM
Para peneliti menerapkan metamorphic testing pada pipeline optimasi compiler C dan menemukan 147 bug yang dikonfirmasi di seluruh GCC dan LLVM. Ini bukan program mainan. Mereka adalah bug miscompilation nyata di mana program C yang benar, ketika dijalankan melalui compiler yang mengoptimalkan, menghasilkan kode mesin yang salah. Beberapa dari bug ini telah ada selama bertahun-tahun. Metamorphic relations-nya sederhana: jika Anda inline sebuah fungsi secara manual, output yang dioptimalkan harus berperilaku sama dengan yang asli. Jika Anda mengacak pernyataan yang independen, hasilnya tidak boleh berubah. Para pengembang compiler mengkonfirmasi dan memperbaiki bug-bug ini.
Compiler shader Vulkan di Google
Tim GraphicsFuzz Google menerapkan metamorphic testing yang dirandomisasi ke dalam produksi untuk Khronos Vulkan Conformance Test Suite. Mereka menghasilkan fragment shader secara acak, menerapkan transformasi yang mempertahankan semantik (seperti membungkus ekspresi dalam fungsi identitas atau menambahkan dead code), dan membandingkan gambar yang dirender di berbagai compiler dan GPU. Ketika dua shader yang seharusnya setara menghasilkan piksel yang berbeda, mereka telah menemukan bug compiler. Tim tersebut membangun seluruh pipeline yang disebut gfauto untuk mereduksi, deduplikasi, dan melaporkan kasus-kasus ini. Mereka menemukan bug dalam ekosistem alat yang mentransformasi, mengoptimalkan, dan memvalidasi shader Vulkan, termasuk driver produksi yang dikirim ke pengguna akhir.
Simulator ADAS yang digunakan oleh OEM otomotif
Sebuah tim menguji tiga platform simulasi ADAS populer, Simulink, CarMaker, dan 51Sim-One Cloud, dengan fokus pada Lane Keeping Assist Systems mereka. Kasus uji biasa lulus di ketiga platform. Tidak ada masalah sama sekali. Kemudian tim menerapkan metamorphic relations geometris: mencerminkan adegan jalan secara horizontal, memutar posisi kendaraan, menerapkan transformasi afin pada marka jalur. Outputnya harus bertransformasi secara prediktabil. Mereka tidak. Bug terungkap di ketiga platform. MathWorks dan IPG Automotive kemudian mengkonfirmasi masalah tersebut. Ini adalah platform yang sama yang digunakan untuk memvalidasi perangkat lunak sebelum masuk ke dalam kendaraan.
Cacat self-driving car dalam kelas yang sama
Dalam salah satu kasus paling menyedihkan, para peneliti menerapkan metamorphic testing pada sistem deteksi objek untuk kendaraan otonom dan menemukan bug dalam pipeline persepsi. Sistem tersebut gagal mengklasifikasikan pejalan kaki dengan benar di bawah input yang diubah secara spesifik. Mereka melaporkannya. Bug yang mereka temukan berada dalam kelas cacat yang sama yang telah dikaitkan dengan tabrakan fatal self-driving car dengan pejalan kaki.
Trade-off: relations bersifat domain-spesifik
Metamorphic testing sangat kuat, tetapi tidak gratis. Bagian yang sulit adalah mengidentifikasi metamorphic relations yang baik. Relation yang buruk memberi Anda kepercayaan diri yang palsu. Relation yang terlalu lemah tidak akan menangkap bug. Relation yang terlalu kuat akan gagal pada perilaku yang benar karena noise floating-point atau non-determinisme.
Merancang relations memerlukan pengetahuan domain. Untuk algoritma jalur terpendek, relations yang baik meliputi: biaya jalur A->B harus sama dengan B->A dalam graf tak berarah; menambahkan konstanta pada setiap bobot tepi harus menambahkan konstanta tersebut dikalikan jumlah tepi ke total biaya jalur. Untuk algoritma pengurutan: membalik daftar yang sudah diurutkan dan mengurutkannya kembali harus memberikan kebalikan dari daftar yang diurutkan asli; setiap elemen dalam output harus muncul dalam input dengan frekuensi yang sama.
Anda tidak bisa menggunakan kembali relations yang sama di seluruh sistem yang tidak terkait. Itulah biayanya.
Aritmatika floating-point adalah jebakan lain. Banyak relations mengasumsikan kesetaraan eksak, tetapi 0.1 + 0.2 != 0.3 dalam IEEE 754. Anda memerlukan perbandingan berbasis toleransi, dan memilih toleransi yang tepat adalah masalahnya sendiri. Terlalu ketat dan Anda mendapatkan false positive. Terlalu longgar dan Anda melewatkan bug nyata.
Cara menambahkan metamorphic testing ke codebase Anda
Anda tidak memerlukan framework. Anda memerlukan disiplin.
Mulailah dengan fungsi-fungsi dalam codebase Anda yang tidak memiliki oracle. Inferensi ML, algoritma optimasi, komputasi geometris, agregasi statistik, dan kode simulasi adalah semua kandidat. Untuk masing-masing, tanyakan: apa yang harus benar tentang output jika saya mengubah input dengan cara yang spesifik dan prediktabil?
Tulis satu metamorphic relation per fungsi uji. Namai dengan jelas. Jalankan di CI bersama unit test Anda. Ketika sebuah relation gagal, perlakukan persis seperti kegagalan test lainnya.
Berikut adalah contoh yang sedikit lebih realistis yang menguji fungsi pathfinding:
import math
def shortest_path_cost(graph, start, end):
"""Returns the cost of the shortest path. Assume implemented."""
pass
def test_shortest_path_undirected_symmetry():
"""MR: In an undirected graph, path cost A->B equals B->A."""
graph = {
'A': [('B', 3.0), ('C', 1.0)],
'B': [('A', 3.0), ('C', 1.0)],
'C': [('A', 1.0), ('B', 1.0)],
}
ab = shortest_path_cost(graph, 'A', 'B')
ba = shortest_path_cost(graph, 'B', 'A')
assert math.isclose(ab, ba, rel_tol=1e-9), f"Symmetry failed: {ab} != {ba}"
def test_shortest_path_subpath():
"""MR: The shortest path cost cannot exceed any specific path's cost."""
graph = {
'A': [('B', 2.0), ('C', 10.0)],
'B': [('C', 2.0)],
'C': [],
}
cost = shortest_path_cost(graph, 'A', 'C')
assert cost <= 10.0, f"Subpath MR failed: {cost} > 10.0"
assert math.isclose(cost, 4.0, rel_tol=1e-9), f"Expected 4.0, got {cost}"
Anda tidak menguji algoritmanya sendiri. Anda menguji implementasi Anda darinya.
FAQ
Apakah metamorphic testing menggantikan unit tests?
Tidak. Ini melengkapinya. Gunakan unit tests ketika Anda mengetahui output yang diharapkan. Gunakan metamorphic tests ketika Anda tidak mengetahuinya.
Bisakah saya menggunakan ini untuk model ML?
Ya, dan ini adalah salah satu area penelitian yang paling aktif. Relations seperti “memutar gambar kucing harus tetap diklasifikasikan sebagai kucing” adalah metamorphic relations. Para peneliti telah menemukan masalah keandalan model dan kesenjangan keadilan menggunakan pendekatan ini.
Bagaimana saya tahu metamorphic relation saya benar?
Anda tidak membuktikannya. Anda berargumen dari spesifikasi atau dari properti matematis domain. Jika relation Anda sendiri buggy, Anda akan mendapatkan false positive. Mulailah dengan properti yang jelas dan tambahkan lebih banyak seiring Anda memperoleh kepercayaan diri.
Bagaimana dengan flaky tests?
Sistem non-deterministik (algoritma probabilistik, kode konkuren, sistem dengan timeout) membuat metamorphic testing lebih sulit. Anda mungkin perlu menjalankan beberapa trial atau menggunakan relations statistik daripada kesetaraan eksak.
Mulailah dengan satu relation
Anda tidak memerlukan gelar PhD untuk menggunakan ini. Pilih satu fungsi dalam sistem Anda di mana Anda saat ini melewatkan pengujian karena memverifikasi output terlalu sulit. Tulis satu metamorphic relation. Jalankan. Jika Anda ingin lebih dalam, perangkat gfauto tim GraphicsFuzz bersumber terbuka, dan survei ACM oleh Segura et al. mengkatalogkan relations di puluhan domain.
Teknik ini menemukan 147 bug compiler, mengonfirmasi cacat pada platform simulasi otomotif, dan memperlihatkan kelas kegagalan persepsi pada mobil self-driving sebelum mereka mencapai jalan. Bug-bugnya nyata. Satu-satunya pertanyaan adalah apakah Anda mencarinya.