Jika suite mutation testing Anda membutuhkan waktu empat jam untuk dijalankan, selamat. Anda telah membuktikan apa yang sudah diduga semua orang: suite pengujian Anda memiliki celah.

Anda tidak akan menjalankannya di CI di setiap push. Tidak ada tim yang melakukannya. Pertanyaannya bukanlah apakah Anda mampu membayar empat jam per commit. Melainkan apakah Anda mampu untuk mengirim kode dengan pengujian yang lolos tetapi sebenarnya tidak memverifikasi apa pun.

100% code coverage adalah metric kosong

Code coverage mengukur baris mana yang dieksekusi selama pengujian. Ia tidak mengukur apakah baris-baris tersebut diuji dengan benar.

Sebuah pengujian dapat mengeksekusi sebuah baris, tidak melakukan assert yang bermakna, dan tetap dihitung sebagai tercakup. Mutation testing memperbaiki ini dengan membuat perubahan kecil pada kode Anda, menjalankan pengujian, dan memeriksa apakah mereka gagal. Jika pengujian lolos setelah kode sengaja dirusak, pengujian tersebut tidak berharga.

Masalahnya adalah skala. Sebuah proyek JavaScript berukuran menengah dengan 10.000 baris kode dan 500 pengujian mungkin menghasilkan 8.000 mutasi. Menjalankan suite pengujian penuh terhadap setiap mutasi secara komputasi mahal. Pada runner CI yang tipikal, dari situlah waktu empat jam Anda berasal.

Menjalankan suite penuh di setiap commit adalah hal yang tidak mungkin. Tetapi itu tidak berarti Anda melewatkan mutation testing sepenuhnya.

Incremental mutation testing adalah satu-satunya pendekatan yang praktis

Alat mutation testing modern mendukung analisis inkremental. Alih-alih memutasi seluruh codebase, mereka hanya memutasi kode yang berubah di pull request saat ini.

Untuk PR tipikal dengan 200 baris kode yang berubah, alat tersebut mungkin menghasilkan 40 hingga 80 mutasi. Menjalankan subset pengujian yang relevan terhadap mutasi-mutasi tersebut membutuhkan waktu menit, bukan jam. Inilah cara tim benar-benar menggunakan mutation testing di CI.

StrykerJS, salah satu framework mutation testing JavaScript yang paling banyak digunakan, mendukung mode inkremental melalui opsi incremental-nya. Ia menyimpan hasil mutasi dalam file incremental.json dan hanya menganalisis ulang file yang berubah.

Berikut konfigurasi stryker.conf.json minimal untuk menjalankan CI inkremental:

{
  "packageManager": "npm",
  "reporters": ["html", "clear-text", "json"],
  "testRunner": "jest",
  "coverageAnalysis": "perTest",
  "incremental": true,
  "incrementalFile": "reports/stryker-incremental.json",
  "mutate": [
    "src/**/*.js",
    "!src/**/*.test.js",
    "!src/**/__tests__/**"
  ],
  "thresholds": {
    "high": 80,
    "low": 60,
    "break": 50
  }
}

Pengaturan coverageAnalysis: perTest sangat penting. Ini memberi tahu Stryker untuk hanya menjalankan pengujian yang mencakup setiap file yang dimutasi, bukan seluruh suite. Ini saja dapat mengurangi waktu runtime satu orde besarnya.

Blok thresholds menentukan kapan build gagal. Dalam contoh ini, mutation score di bawah 50% merusak pipeline CI. Skor antara 50% dan 60% menghasilkan peringatan. Di atas 80% adalah hijau.

Tiga pola CI yang benar-benar berfungsi

Tim yang berhasil menggunakan mutation testing tidak mencoba menjalankannya seperti unit test. Mereka menggunakan salah satu dari tiga pola.

Full run nightly di branch main. Suite mutasi lengkap berjalan sekali per hari, biasanya di malam hari. Hasilnya dipublikasikan ke dashboard dan dilacak dari waktu ke waktu. Ini menangkap masalah kualitas pengujian secara sistemik tanpa memblokir pengembangan sehari-hari. Tim meninjau tren, bukan skor individual.

Run inkremental di pull request. Hanya file yang berubah yang dimutasi. Job CI menambahkan 3 hingga 8 menit ke pipeline PR. Jika mutation score untuk kode yang berubah turun di bawah threshold, PR diblokir. Di sinilah mutation testing menangkap nilainya: pada titik di mana kode baru memasuki codebase.

Pre-release gate sebelum deployment besar. Beberapa tim menjalankan analisis mutasi penuh sebelum shipping ke produksi atau sebelum merilis versi baru. Ini diperlakukan sebagai quality checkpoint, mirip dengan security audit atau performance regression test. Tidak untuk setiap release, tetapi untuk yang penting.

Tim yang mendapatkan nilai terbanyak menggabungkan dua pola pertama. Jalankan nightly melacak kesehatan seluruh codebase. Jalankan inkremental di PR menegakkan kualitas pada kode baru.

Mutation score bukanlah sebuah target

Di sinilah mutation testing menjadi berbahaya secara politis. Jika Anda mempublikasikan mutation score di seluruh tim dan mengaitkannya dengan performance review, engineer akan mengoptimalkan untuk metric tersebut.

Mereka akan menulis pengujian yang membunuh mutasi tanpa menguji perilaku aktual. Mereka akan berargumen bahwa equivalent mutants, yang secara semantik identik dengan kode asli, harus dikecualikan dari penilaian. Mereka akan menghabiskan waktu berjam-jam men-tweak threshold alih-alih menulis pengujian yang berguna.

Mutation testing adalah alat diagnostik, bukan papan peringkat. Skor adalah sinyal untuk menyelidiki, bukan target untuk dicapai.

Pendekatan yang lebih berguna adalah melacak tren mutation score dari waktu ke waktu dan memperlakukan skor rendah pada kode baru sebagai pembuka percakapan. “PR ini memperkenalkan 12 mutasi dan hanya 4 yang terbunuh. Mari kita lihat apa yang hilang.” Itu jauh lebih berharga daripada dashboard yang menampilkan 73% di seluruh repository.

Workflow GitHub Actions yang berfungsi

Di bawah ini adalah workflow GitHub Actions yang siap produksi yang menjalankan mutation testing inkremental di pull request dan menyimpan status inkremental antar run.

name: Mutation Testing

on:
  pull_request:
    branches: [main]

jobs:
  stryker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Download previous incremental report
        uses: actions/download-artifact@v4
        with:
          name: stryker-incremental
          path: reports/
        continue-on-error: true

      - name: Run Stryker (incremental)
        run: npx stryker run

      - name: Upload incremental report for next run
        uses: actions/upload-artifact@v4
        with:
          name: stryker-incremental
          path: reports/stryker-incremental.json
        if: always()

Detail kuncinya adalah fetch-depth: 0. Stryker membutuhkan riwayat Git lengkap untuk menentukan file mana yang berubah antara branch PR dan branch target. Tanpanya, mode inkremental akan kembali ke run penuh.

Workflow mengunduh artifact stryker-incremental.json sebelumnya sebelum dijalankan. Jika artifact tidak ada, run pertama secara efektif adalah analisis penuh. Run berikutnya menggunakan hasil yang di-cache.

if: always() pada langkah upload memastikan status inkremental disimpan bahkan jika job mutation testing gagal karena pelanggaran threshold. Tanpa ini, PR berikutnya dimulai dari awal.

Equivalent mutants masih menjadi masalah

Tidak ada alat mutation testing yang dapat secara andal mendeteksi equivalent mutants. Ini adalah mutasi yang mengubah sintaks kode tetapi tidak semantiknya. Contoh klasik adalah mengganti a = b + c dengan a = c + b dalam operasi komutatif. Mutasinya secara teknis berbeda, tetapi perilakunya identik.

Equivalent mutants membuang waktu CI dan membuat frustrasi engineer. Keadaan seni saat ini adalah eksklusi manual melalui konfigurasi spesifik alat. Stryker memungkinkan Anda mengabaikan mutator atau file tertentu. PIT untuk Java mendukung excludedMethods dan excludedClasses.

Tidak ada solusi yang sempurna. Tim yang menggunakan mutation testing menerima tingkat noise dasar dan secara berkala meninjau daftar pengecualian mereka.

Apakah tim Anda perlu repot-repot?

Mutation testing tidak gratis. Ia membutuhkan compute CI, konfigurasi alat, dan pemeliharaan berkelanjutan terhadap threshold dan pengecualian. Ini berlebihan untuk prototipe atau proyek dengan dua engineer.

Usahanya menjadi sebanding ketika Anda memiliki codebase yang cukup besar sehingga kualitas pengujian menurun tanpa pengawasan, dan tim yang cukup besar sehingga tidak semua orang meninjau setiap PR secara detail. Jika Anda pernah menemukan bug di produksi yang seharusnya tertangkap oleh pengujian, dan pengujiannya ada tetapi sebenarnya tidak melakukan assert apa pun, mutation testing akan menangkapnya.

Mulailah dengan run inkremental di PR untuk layanan Anda yang paling kritis. Lacak trennya selama sebulan. Jika angka-angkanya memberi tahu Anda sesuatu yang berguna, perluas. Jika tidak, Anda hanya kehilangan beberapa menit CI, bukan empat jam.

Untuk tim yang baru memulai, Stryker handbook memiliki panduan khusus platform untuk JavaScript, C#, dan Scala. Untuk proyek JVM, PIT tetap menjadi standar. Keduanya mendukung analisis inkremental out of the box.