A focused demonstration of mutation testing — the technique that measures how good your tests are, not just how much code they touch. It uses StrykerJS to mutate a realistic TypeScript pricing/discount library and re-runs the Vitest suite against every mutant.
Line/branch coverage tells you which code ran during tests — not whether your assertions would actually catch a bug. Mutation testing closes that gap:
- Stryker introduces small faults ("mutants") into the source — flipping
>=to>,+to-,&&to||, removing a branch, etc. - It re-runs the test suite against each mutant.
- If a test fails, the mutant is killed (good — your tests caught the change). If all tests still pass, the mutant survived (a blind spot in your suite).
The mutation score = killed / total mutants. A high score is far stronger evidence of suite quality than a high coverage percentage.
src/pricing.ts— a non-trivial domain library (subtotal, tiered membership discounts with a promo-code cap, discounted-subtotal-based free shipping, tax, and a full breakdown). It is deliberately rich in the operators and boundaries mutation testing targets.src/pricing.test.ts— a thorough Vitest suite written to kill mutants: boundary cases (at/just-below/just-above thresholds), each branch, the discount cap, and integration breakdowns.stryker.config.json— StrykerJS config with a break threshold of 80%, so CI fails if the suite ever weakens.
# 1. Install dependencies
npm install
# 2. Run the unit tests
npm test
# 3. Run mutation testing (produces reports/mutation/index.html)
npm run test:mutation| Command | What it does |
|---|---|
npm test |
Run the Vitest unit suite once |
npm run test:watch |
Vitest in watch mode |
npm run coverage |
Unit tests with a V8 coverage report |
npm run test:mutation |
Run StrykerJS mutation testing + HTML report |
npm run typecheck |
Type-check with tsc --noEmit |
After npm run test:mutation, Stryker prints a summary and writes an interactive
report to reports/mutation/index.html. Surviving mutants are highlighted in the
source so you can see exactly which change your tests failed to detect — then add a
test to kill it. This suite scores ~98%, well above the 80% break threshold.
The report shows a single survivor: rate > MAX_DISCOUNT_RATE mutated to
rate >= MAX_DISCOUNT_RATE. This is an equivalent mutant — it cannot be killed
by any test. At the only boundary value (rate === 0.25), clamping to 0.25 is a
no-op, so > and >= produce identical output. Recognising equivalent mutants (and
not chasing an artificial 100%) is part of using mutation testing well.
- Mutation testing is CPU-intensive because the suite runs once per mutant; it is typically run on focused, high-value modules (like this pricing engine) rather than an entire codebase on every commit.
- The library is pure and dependency-free by design, which keeps mutants fast to evaluate and the example easy to follow.
Darrius Jones (@Djones-qa · LinkedIn)
MIT © Darrius Jones