Skip to content

Repository files navigation

Mutation testing PoC with Jest + Stryker

A tiny proof of concept showing what mutation testing adds on top of code coverage.

Both modules in src/ have 100% line, branch, statement and function coverage. Only one of them has a test suite that actually verifies the behavior. Mutation testing is what tells them apart.

Setup

npm install

The demo, in four commands

npm test                         # 9 tests, all green
npm run test:coverage            # 100% coverage on both files
npm run test:mutation            # mutation score: 91.49%, 4 survived, EXITS 1
npm run test:mutation:solution   # mutation score: 100%, all killed, exits 0

The HTML report is written to reports/mutation/mutation.html.

The quality gate

stryker.config.json sets thresholds.break: 95, so a weak suite fails the build the same way a failing test would:

ERROR MutationTestReportHelper Final mutation score 91.49 under breaking
threshold 95, setting exit code to 1 (failure).

high: 100 / low: 80 only color the report; break is the one that decides the exit code. Setting break: 100 is usually a mistake, since equivalent mutants make it unreachable, so teams pick a realistic floor and ratchet it up over time.

.github/workflows/ci.yml wires this into GitHub Actions: coverage runs first (green), the mutation gate runs second (red), and the HTML report is uploaded as an artifact with if: always(), because the report is exactly what you need when the gate is what failed.

What is a mutant?

Stryker makes a small change to the production code (a mutant) and re-runs the tests:

  • tests fail → the mutant is killed ✅ (your tests noticed the change)
  • tests pass → the mutant survived ❌ (that line is executed, but not verified)

mutation score = killed / total mutants

Mutators used here include: conditional boundaries (>=>), equality operators, arithmetic operators (*/), boolean/string literals, and block removal.

The two modules

src/leapYear.js: strong suite, 100% score, 21/21 killed

The tests cover every equivalence class and the boundaries between them: 2000 (÷400), 1900 (÷100 but not ÷400), 2024 (÷4 but not ÷100), 2023, and a non-integer input asserted with the exact error message.

src/discount.js: weak suite, 84.62%, 4 mutants survive

The tests pass and cover every line, but 4 mutants survive:

Survivor Why it survives
total < 0total <= 0 0 is never tested, so rejecting it goes unnoticed
total >= 100total > 100 the discount is tested at 200, never at exactly 100
price < 50price <= 50 shipping is tested at 10, never at exactly 50
'total must not be negative''' the test asserts only toThrow(RangeError), not the message

Three of the four are off-by-one bugs at a business rule boundary, the kind of defect that reaches production with a green coverage badge.

src/discount.solution.test.js adds the four tests that kill them. It is skipped by default and enabled with SOLUTION=1 (see jest.config.js), so you can run the "before" and "after" side by side.

Talking points for an interview about mutation testing

  • Coverage asks "was this line executed?"; mutation testing asks "would a change to this line be caught?" It measures the assertions, not the execution.
  • Surviving mutants are actionable: each one is either a missing test case, a weak assertion, or dead/redundant code you can delete.
  • Equivalent mutants are the known false positive: a mutation that cannot change observable behavior (e.g. mutating a pure logging call), so no test can kill it. That's why 100% is not always the right goal. Stryker's thresholds and // Stryker disable comments exist for this.
  • Cost: the test suite runs once per mutant. This PoC uses coverageAnalysis: "perTest", so only the tests covering the mutated line run (the report shows ~2.9 tests per mutant instead of all 9). Other levers: --incremental and --since in CI, so only changed files are mutated.
  • Where it pays off: business rules, pricing, validation, permissions, logic dense in boundaries. Less so in glue and I/O code.

Files

About

This is a project to demonstrate mutation testing using Stryker and Jest. It includes a simple implementation of a function and its corresponding tests, along with the necessary configuration for running mutation tests.

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages