Menerapkan skor mutasi tunggal di seluruh codebase adalah cara terbaik untuk membuat timmu benci testing.

Jalankan PIT atau Stryker terhadap repo tipikal dan kamu akan melihat pola yang sama: module autentikasi mencetak 40%, utilitas string mencapai 95%, dan lapisan ORM-mu berada di kisaran 60-an. Respons refleks adalah menetapkan gate global di, misalnya, 70% dan memblokir setiap PR yang turun di bawahnya. Dua sprint kemudian, seseorang menonaktifkan pemeriksaan di CI dan menyalahkannya pada “mutator yang flaky.”

Masalah sesungguhnya bukan pada tool-nya. Masalahnya adalah berpura-pura bahwa semua kode memiliki blast radius yang sama ketika seorang mutant bertahan hidup.

Apa yang sebenarnya diukur oleh mutation testing

Code coverage memberitahumu baris mana yang dijalankan. Mutation testing memberitahumu apakah tes-mu akan menyadari jika baris-baris tersebut berubah.

Sebuah mutation framework memperkenalkan kesalahan kecil (mutant) ke dalam source-mu. Ia bisa membalik > menjadi <, menghapus pemanggilan method, atau mengubah nilai return. Jika test suite-mu menangkap perubahan tersebut, mutant tersebut terbunuh. Jika tetap lolos, mutant tersebut bertahan hidup. Skor mutasimu adalah persentase mutant yang terbunuh.

Seorang mutant yang bertahan hidup dalam perbandingan hash password adalah bug keamanan yang menunggu produksi. Seorang mutant yang bertahan hidup dalam helper capitalizeFirstLetter adalah, paling buruk, label UI yang sedikit aneh.

Memperlakukan mereka sama adalah kesalahan tim.

Mengapa auth pantas mendapatkan gate 90%+

Kode autentikasi dan otorisasi memiliki dua properti yang membuatnya ideal untuk mutation testing yang agresif.

Pertama, logikanya biasanya diskrit dan seperti state machine. Apakah token sudah kadaluarsa? Apakah role berada dalam set yang diizinkan? Apakah tanda tangan terverifikasi? Setiap cabang memiliki implikasi keamanan yang jelas, dan masing-masing harus diuji.

Kedua, biaya dari seorang mutant yang bertahan hidup adalah bencana. Satu boolean yang terbalik dalam pengecekan role dapat mengekspos endpoint admin. Satu not yang terlewat dalam rutin validasi token dapat menerima JWT yang dipalsukan. Ini bukan teori. Database CVE penuh dengan bypass auth yang disebabkan oleh kesalahan logika yang seharusnya tertangkap oleh mutation testing.

Di Sentry, kami menegakkan skor mutasi 90% untuk apa pun di module authn/ dan authz/. Apa pun di bawah itu gagal CI. Tidak ada override, tidak ada “kita akan memperbaikinya di sprint berikutnya.” Modulnya cukup kecil sehingga ini dapat dicapai tanpa menulis 40 baris tes untuk setiap baris kode produksi.

Begini penampakannya dalam praktik. Ini adalah rutin validasi JWT yang disederhanakan:

import time
from typing import Optional

def verify_token(token: dict, expected_aud: str, leeway: int = 30) -> bool:
    now = time.time()

    if token.get("aud") != expected_aud:
        return False

    exp = token.get("exp")
    if exp is not None and now > exp + leeway:
        return False

    return True

Sebuah mutation framework bisa membalik > menjadi >= dalam pengecekan kedaluwarsa. Tanpa tes yang menggunakan token yang kadaluarsa tepat pada now + leeway, mutant tersebut bertahan hidup. Itu berarti tes-mu sebenarnya tidak memverifikasi batasannya. Pada coverage mutasi 90%, tes tersebut ada.

Kode utilitas bisa bertahan dengan 60%

StringUtils, DateHelpers, dan MathExtensions-mu berada di ujung lain spektrum.

Module-module ini cenderung pure, banyak digunakan kembali, dan mudah untuk dipikirkan. Seorang mutant yang bertahan hidup dalam truncate(str, maxLen) yang mengubah > menjadi >= mungkin memotong satu karakter ekstra. Itu adalah keanehan UI, bukan incident keamanan.

Matematika risiko-imbalan bergeser. Module-module ini sering memiliki puluhan fungsi kecil. Mengejar coverage mutasi 90% berarti menulis tes untuk setiap varian off-by-one dalam padLeft. Tes menjadi lebih panjang dari kode yang mereka lindungi, dan beban pemeliharaan mulai melebihi nilainya.

Kami menetapkan lantai 60% untuk module utilitas. Itu menangkap celah yang jelas (null checks yang hilang, nilai return yang salah) tanpa memaksa tim untuk menguji secara ekshaustif setiap permutasi pemotongan string.

Kuncinya adalah jujur tentang apa artinya 60%. Artinya “kita sudah menguji kasus umum dan kegagalan yang jelas.” Bukan berarti “kode ini tidak penting.” Jika fungsi utilitas digunakan di jalur yang sensitif terhadap keamanan, ia mewarisi threshold yang lebih tinggi dari konsumennya.

Titik tengah: business logic

Sebagian besar kodemmu berada di antara kedua kutub ini. Pemrosesan pembayaran, validasi data, orchestration workflow. Module-module ini mempengaruhi kebenaran dan kepercayaan pengguna, tetapi seorang mutant yang bertahan hidup biasanya tidak akan menyerahkan database-mu kepada penyerang.

Kami menggunakan sistem bertingkat:

Tipe moduleThreshold mutasiRasional
AuthN / AuthZ90%Blast radius tinggi, logika diskrit
Business logic75%Kritis untuk kebenaran, kompleksitas sedang
Utilities / helpers60%Blast radius rendah, banyak digunakan kembali, fungsi sederhana
Generated / boilerplateDikecualikanJangan tes kode yang tidak kamu tulis

Ini bukan aturan yang kaku. Sebuah module perhitungan pembayaran mungkin naik ke 85%. Sebuah helper JSON yang banyak digunakan mungkin naik kelas ke 75% jika dikonsumsi oleh kode auth. Tingkatannya adalah titik awal, bukan sangkar.

Cara mengimplementasikan tiered mutation gates

Stryker dan PIT keduanya mendukung konfigurasi per-module. Begini cara kami menghubungkannya ke proyek Python menggunakan mutmut dengan konfigurasi kustom:

# mutation_config.py
THRESHOLDS = {
    "src/authn/": 90,
    "src/authz/": 90,
    "src/billing/": 85,
    "src/workflows/": 75,
    "src/utils/": 60,
}

EXCLUDE_PATHS = [
    "src/generated/",
    "src/migrations/",
]

Di CI, sebuah skrip kecil membaca konfigurasi ini dan menjalankan mutation tester per module:

#!/usr/bin/env bash
# ci/check-mutation.sh
set -e

python -m mutmut run --paths-to-mutate=src/authn/
python -m mutmut results || true
python -m mutmut run --paths-to-mutate=src/utils/
python -m mutmut results || true

python ci/verify_thresholds.py

Skrip verifikasi memeriksa skor setiap module terhadap threshold-nya. Jika src/authn/ mencetak 87%, build gagal dengan pesan yang jelas: authn/ scored 87%, threshold is 90%.

Untuk Stryker (JavaScript/TypeScript), gunakan stryker.conf.js dengan grup mutator:

// stryker.conf.js
module.exports = {
  thresholds: {
    high: 90,
    low: 75,
    break: null, // we handle this per-module
  },
  mutate: [
    "src/auth/**/*.ts",
    "src/billing/**/*.ts",
    "src/utils/**/*.ts",
  ],
  ignorePatterns: ["src/generated/**"],
};

Kami membungkus Stryker dalam skrip yang menjalankannya tiga kali dengan path glob yang berbeda dan menegakkan threshold per-direktori setelah setiap run. Sedikit klunky, tapi berhasil.

Jebakan mengejar 100%

Beberapa tim melihat mutation testing sebagai permainan untuk dimenangkan. Mereka menulis tes yang ada hanya untuk membunuh mutant, bukan untuk memverifikasi perilaku.

Contoh terburuk adalah menguji bahwa pesan exception tertentu mengandung substring, hanya agar mutant yang mengubah teks pesan terbunuh. Tes itu tidak menambah nilai. Ia tidak memverifikasi exception dilempar pada waktu yang tepat, atau tipe yang benar diangkat. Ia hanya memverifikasi string-nya.

Jika kamu menemukan diri menulis tes hanya untuk menaikkan persentase, kamu sudah membalikkan tujuannya. Mutation testing adalah alat diagnostik, bukan papan peringkat. Skor memberitahumu di mana harus mencari. Ia tidak memberitahumu kapan kamu selesai.

Apa yang kami pelajari dengan cara yang sulit

Kami memulai dengan gate global 80%. Dalam sebulan, tiga tim menonaktifkannya di branch fitur “sementara.” Dua dari penonaktifan sementara itu menjadi permanen.

Masalahnya bukan angkanya. Masalahnya adalah 80% terlalu rendah untuk kode auth (kita melewatkan bug pengecekan role yang sampai ke staging) dan terlalu tinggi untuk module utilitas 4.000 baris (tim menghabiskan dua minggu menulis tes untuk varian isValidEmail).

Setelah kami membagi menjadi tingkatan, adopsinya bertahan. Tim auth menerima target 90% karena ruang lingkupnya terbatas. Tim platform menerima 60% untuk utilitas karena dapat dicapai tanpa kegilaan. Pendekatan bertingkat mengubah mutation testing dari hukuman menjadi percakapan tentang risiko.

Di mana memulai

Jika kamu memperkenalkan mutation testing ke codebase yang sudah ada, jangan tetapkan gate apa pun di minggu pertama. Jalankan tool-nya, lihat skornya, dan tanyakan: di mana seorang mutant yang bertahan hidup akan paling menyakitkan?

Mulai dari auth. Tetapkan 90% di sana, buat hijau, dan buktikan nilainya. Perluas ke business logic setelah tim mempercayai sinyalnya. Pertahankan utilitas pada batas yang lebih rendah atau kecualikan sepenuhnya sampai kamu membangun kebiasaan.

Dan ingat: skor 60% dengan tes yang jujur mengalahkan skor 95% dengan tes yang ditulis untuk mengakali mutator. Tujuannya adalah menangkap bug nyata, bukan mengesankan dasbor metrikmu.

Jika kamu ingin mencoba ini sendiri, mutmut untuk Python dan Stryker untuk JavaScript keduanya mendukung pola per-direktori yang dijelaskan di atas. Mulai dari yang kecil. Satu module auth. Satu minggu. Lihat apa yang bertahan hidup.