Large Language Models (LLM) mempercepat penulisan kode implementasi dan unit test secara signifikan. Namun, akselerasi ini memicu fenomena performative productivity: test suite yang dihasilkan AI mampu mengeksekusi semua baris kode hingga mencapai 100% line coverage, tetapi gagal memverifikasi correctness logika bisnis ketika terjadi penyimpangan nilai batas (boundary conditions). Metrik code coverage standar hanya mencatat baris mana yang dieksekusi, bukan apakah assertion yang ditulis sanggup mendeteksi bug.

Untuk mengatasi ilusi kualitas kode ini, pipeline Continuous Integration (CI) membutuhkan verifikasi berbasis mutation testing. Pendekatan ini menyuntikkan kesalahan sengaja (mutant) ke dalam kode sumber untuk menguji ketangguhan test suite.

Anatomi Masalah: Test Coverage Semu dari LLM

Model AI sering menulis assertion parsial yang mengabaikan operator perbandingan atau off-by-one errors. Perhatikan contoh fungsi penentuan diskon berbasis tier berikut:

// src/discount.ts
export function calculateDiscount(orderAmount: number, isVip: boolean): number {
  if (orderAmount >= 1000 || isVip) {
    return 0.15;
  }
  return 0.0;
}

Ketika diminta membuatkan test suite, LLM kerap memproduksi unit test seperti ini:

// test/discount.spec.ts
import { calculateDiscount } from '../src/discount';

describe('calculateDiscount', () => {
  it('harus memberi diskon untuk order besar VIP', () => {
    const discount = calculateDiscount(1500, true);
    expect(discount).toBeGreaterThan(0);
  });

  it('harus mengembalikan 0 untuk non-VIP order kecil', () => {
    const discount = calculateDiscount(500, false);
    expect(discount).toBe(0.0);
  });
});

Coverage report standar (Istanbul/c8) mencatat 100% branch dan line coverage. Namun assertion tersebut rapuh:

  • Jika operator >= diubah menjadi >, test tetap lolos karena input pengujian bernilai 1500.
  • Jika operator || diubah menjadi &&, test pertama tetap lolos karena kedua kondisi bernilai benar (1500 dan true).
  • Assertion expect(discount).toBeGreaterThan(0) tidak memvalidasi nilai eksak 0.15, sehingga mutasi nilai return 0.15 menjadi 0.01 tetap menghasilkan status green check.

Implementasi Mutation Testing dengan Stryker di CI

Mutation testing memodifikasi syntax tree (AST) source code: mengganti >= dengan >, membalik boolean, atau menghapus blok statement. Jika test suite gagal (red), mutant dinyatakan killed. Jika test suite tetap lolos (green), mutant dinyatakan survived.

Skor mutasi dihitung menggunakan formula: (Mutant Terbunuh / Total Mutant Terdeteksi) * 100.

1. Konfigurasi Stryker Mutator

Instalasi pada proyek TypeScript menggunakan Jest/Vitest:

npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner

Simpan konfigurasi pada stryker.config.json:

{
  "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
  "packageManager": "npm",
  "reporters": ["clear-text", "progress", "dashboard"],
  "testRunner": "jest",
  "coverageAnalysis": "perTest",
  "mutate": [
    "src/**/*.ts",
    "!src/**/*.d.ts",
    "!src/**/index.ts"
  ],
  "thresholds": {
    "high": 85,
    "low": 70,
    "break": 75
  }
}

Properti thresholds.break: 75 memastikan proses keluar dengan exit code error jika skor mutasi turun di bawah 75%, menggagalkan pipeline CI secara otomatis.

2. Konfigurasi GitHub Actions Pipeline

Tambahkan job mutation testing ke dalam workflow CI:

name: Quality Assurance

on:
  pull_request:
    branches: [main]

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

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci
      - run: npm test -- --coverage
      - name: Run Mutation Testing on Diff
        run: npx stryker run --mutate "$(git diff --name-only origin/main...HEAD | grep '^src/.*\.ts$' | paste -sd, -)"

Strategi Optimasi Runtime Mutation Testing di CI

Kelemahan mendasar dari mutation testing adalah kompleksitas komputasional: menjalankan seluruh test suite sebanyak N mutant memerlukan waktu berjam-jam pada basis kode skala besar. Terapkan strategi seleksi berikut agar runtime tetap berada pada batas toleransi deployment (di bawah 10 menit):

1. Diff-Based Mutation (Pull Request Scope)

Jangan mutasi seluruh direktori proyek pada setiap pull request. Batasi mutasi hanya pada file atau baris yang berubah menggunakan referensi Git diff, seperti yang diimplementasikan pada flag --mutate dalam konfigurasi CI di atas.

2. In-Memory Static Analysis (Coverage Analysis)

Gunakan opsi coverageAnalysis: "perTest". Tool tidak akan mengeksekusi suite pengujian secara acak; ia hanya mengeksekusi unit test spesifik yang menyentuh baris kode tempat mutant disuntikkan. Jika mutasi terjadi di discount.ts, test suite untuk auth.ts tidak akan dijalankan.

3. Konkurensi Berdasarkan Resource CPU

Stryker secara default menggunakan alokasi worker thread: Math.floor(CPU - 1). Di environment CI virtual seperti GitHub Runners (biasanya 2 vCPU), batasi concurrency untuk menghindari degradasi memori dan timeout false-positive:

npx stryker run --concurrency 2

Kesimpulan

Kode yang dihasilkan LLM terlihat meyakinkan secara sintaksis dan sering kali lolos uji line coverage tanpa perlindungan edge-case yang memadai. Mengandalkan coverage tradisional untuk kode hasil AI menimbulkan risiko regresi fungsional yang fatal di produksi. Mutation testing memindahkan validasi dari sekadar mengecek eksekusi baris menjadi menguji kekuatan assertion, memastikan setiap baris kode AI yang di-commit ke branch utama diverifikasi oleh test suite yang tangguh.