Mutation Testing — Testing

100% Coverage Tidak Sama dengan Test yang Baik Line coverage 100% cuma berarti setiap baris pernah dieksekusi test. Tidak berarti test itu benar. Contoh: functi

100% Coverage Tidak Sama dengan Test yang Baik

Line coverage 100% cuma berarti setiap baris pernah dieksekusi test. Tidak berarti test itu benar. Contoh:

function isAdult(age) {
  return age >= 18;
}

test("adult check", () => {
  isAdult(20); // tidak ada assertion! tapi coverage 100%
});

Test di atas memberi 100% coverage, tapi apapun implementasi isAdult akan lulus — termasuk return age > 18 (bug off-by-one). Mutation testing dirancang untuk menangkap kelemahan seperti ini.

Cara Kerja Mutation Testing

  1. Framework memodifikasi kode aslimu dengan mutasi kecil (mutant)
  2. Jalankan test suite terhadap tiap mutant
  3. Jika test gagal (detect mutasi) → mutant killed (bagus)
  4. Jika test tetap lulus → mutant survived (test-mu lemah!)

Jenis Mutation Operators

KategoriOriginalMutant
Arithmetica + ba - b, a * b
Conditional (relational)a >= ba > b, a <= b, a == b
Logicala && ba || b
Boolean literalreturn truereturn false
String"hello"""
Incrementi++i--
Block removalbody functiondihapus/return undefined

Tools

Contoh Konfigurasi Stryker

// stryker.conf.json
{
  "packageManager": "npm",
  "testRunner": "vitest",
  "mutate": ["src/**/*.ts", "!src/**/*.test.ts"],
  "thresholds": { "high": 80, "low": 60, "break": 50 },
  "reporters": ["html", "progress"]
}

Mutation Score

Metrik utamanya:

Mutation Score = (killed mutants / total mutants) × 100%

Contoh: dari 100 mutant, 85 killed, 15 survived → skor 85%. Skor ini lebih bermakna daripada line coverage karena menguji kualitas assertion, bukan sekadar eksekusi.

Target realistis: 75-85% untuk project produksi. 100% sering tidak praktis karena ada equivalent mutant — mutasi yang tidak mengubah behaviour (mis. i < arr.length vs i != arr.length).

Survived Mutation sebagai Signal

Mutation survived adalah gold untuk memperbaiki test suite. Contoh hasil report:

Survived mutant di src/pricing.ts:12
  Original: if (total >= 100000) applyDiscount();
  Mutant:   if (total > 100000) applyDiscount();

→ Test-mu tidak menguji edge case total === 100000
→ Tambah: it("tepat di batas 100000, harus diskon", ...)

CI Gating

# Stryker: fail CI jika mutation score turun
npx stryker run --break 75

# Infection: --min-msi=80 artinya minimum 80%
vendor/bin/infection --min-msi=80 --min-covered-msi=85

Kapan & Kapan TIDAK Pakai

Yang akan kamu pelajari