Tes yang tak bisa gagal
Assert exit code bisa dilalui jalur lain yang tidak diuji. Perbaikannya mengikat assert ke output yang diperlebar.
Ringkasan
Test CLI itu cuma ngecek exit code, jadi walau fitur ekspansi dihapus, test tetap hijau alias mutannya selamat. Artinya testnya nggak bisa gagal dan lulus gara-gara jalur lain. Solusinya tangkap stdout terus assert hash beneran melebar jadi ID tugas, biar ikut mati kalau fiturnya dimatikan.
Semua indikator hijau untuk fitur baru di sebuah CLI pelacak: seed berupa hash commit memperlebar dirinya sendiri dengan menarik ID tugas dari subjek commit, lalu memindai ulang berkas papan, keputusan, dan log sekali. Assert yang mengawalnya sederhana, assert run(...) == 0. Logikanya terasa karantina aman: jika proses kembali nol, ekspansi pasti berjalan.
Tinjauan mematahkan asumsi itu dengan satu kalimat: assertion-nya vacuous. Di dalam fixture, pintu log commit selalu cocok dengan hash commit fixture itu sendiri. Jadi run(...) kembali nol bukan karena ekspansi bekerja, melainkan karena jalur tanpa ekspansi sudah memenuhi kondisi yang sama. Bukti ritualnya kejam: blok ekspansi dibuang dari salinan berkas, suite dijalankan ulang, dan test harus gagal. Test tersebut tetap hijau. Fitur bisa dihapus tanpa satu pun test menyala.
Test yang tidak bisa gagal bukan test
Kasus ini adalah mutasi memertahankan hidup secara manual. Dokumen Stryker merumuskan prinsipnya: mutation testing memasukkan perubahan ke kode lalu menjalankan test terhadap kode itu, dan "If your tests fail then the mutant is killed . If your tests passed, the mutant survived ." [4]. Mutan di sini adalah penghapusan fitur ekspansi, dan mutannya selamat. Selamatnya berarti ada test yang hilang, bukan kode yang benar.
Terjebaknya spesifik: exit code hanya membuktikan program tidak crash. Untuk fitur yang menghasilkan perubahan teramati, pernyataan "berjalan tanpa error" tidak sama dengan "melakukan tugasnya". Kriteria keberhasilan yang terlalu longgar membiarkan jalur lain mencuri kelulusan.
Mengikat assert ke bukti spesifik
Perbaikan menggeser validasi dari exit code ke keadaan yang diperlebar. Keluaran standar ditangkap lewat contextlib.redirect_stdout, utilitas yang definisinya di dokumentasi Python: "Context manager for temporarily redirecting sys.stdout to another file object ." [6], diarahkan ke io.StringIO. Baris pertama keluaran kemudian di-assert: hash seed harus sudah melebar menjadi ID tugas yang sebelumnya hanya ada di papan fixture. Pada pytest, pola yang setara tersedia siap pakai lewat fixture capture; readouterr() kembali "namedtuple with two attributes, out and err ." [5], dan untuk CLI murni stdout yang setara capfd membaca di level file descriptor. Identitas alat tidak penting; pentingnya, uji output, bukan status keluar.
Assert baru ini diperiksa ulang dengan cara yang sama seperti yang lama dikutuk: tanpa blok ekspansi, ekspansi file sumber yang di-strip dari salinan repo, suite harus dan memang gagal. Dua arah sekarang tertutup, fitur hidup test menyala, fitur mati test juga menyala.
Fitur yang diuji sendiri dibatasi desainnya supaya kegagalannya mudah diduga. Ekspansi berjalan paling banyak satu pass: hasil pemindaian kedua tidak pernah menjadi seed baru, sehingga rantai terjangkau O(2x) dan tidak mabuk. Regex ID tugas sengaja dibatasi pada pola kartu B dan kartu ISSUE; memasukkan teks lain membuat arah hash berkeliaran. Batas seed 16 menjaga keluaran tetap bisa dibaca manusia.
Sisa keputusan desain ini satu pelajaran umum: asert test pada efek yang bisa diamati. Exit code untuk crash, output untuk perubahan perilaku. Sebuah suite hijau boleh jadi sedang bercerita tentang jalur lain yang tidak disadari siapa pun.
Sumber
- Stryker Mutator documentation, "What is mutation testing?" (diakses 2026-10-12): "Mutation testing introduces changes to your code, then runs your unit tests against the changed code."; "If your tests fail then the mutant is killed. If your tests passed, the mutant survived."
- pytest documentation, "How to capture stdout/stderr output" (diakses 2026-10-12): "The return value of readouterr() is a namedtuple with two attributes, out and err."
- Python documentation, contextlib (diakses 2026-10-12): "Context manager for temporarily redirecting sys.stdout to another file object."