Reading and Interpreting Mutation Reports — using mutation testing to verify test quality beyond code coverage.
Reading and Interpreting Mutation Reports addresses a fundamental problem with code coverage: 100% coverage can coexist with zero meaningful assertions. Mutation testing intentionally introduces bugs into your code (mutations) and verifies your tests catch them. If a test suite cannot detect a bug, it is not adequately testing the behavior.
+ to -, a > to >=)1Mutation Score = (Killed Mutants / Total Mutants) × 100
2
3Example:
4 Total mutants: 150
5 Killed: 120
6 Survived: 30
7 Mutation Score: 80%
8
9Target: >75% for business logic, >60% for general code1# Install Stryker
2npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner
3
4# Initialize configuration
5npx stryker init
6
7# Run mutation testing
8npx stryker run
9
10# Run on a specific file
11npx stryker run --mutate src/utils/pricing.ts1// Arithmetic Operator Mutation
2return a + b; // → a - b, a * b, a / b, a % b
3
4// Test that kills it:
5expect(add(3, 2)).toBe(5); // 3 - 2 = 1, not 5 ✓
6
7// Logical Operator Mutation
8if (isAdmin && isVerified) // → isAdmin || isVerified
9
10// Test that kills it:
11expect(canAccess(true, false)).toBe(false); // || would give true ✓
12
13// Relational Operator Mutation
14if (age >= 18) // → age > 18, age <= 18, age === 18
15
16// Test that kills boundary:
17expect(isAdult(18)).toBe(true); // > 18 would give false ✓
18
19// Return Value Mutation
20return price * discount; // → return undefined
21
22// Test that kills it:
23expect(calculate(100, 0.2)).toBe(20); // undefined !== 20 ✓1// stryker.config.mjs
2export default {
3 testRunner: 'jest',
4 coverageAnalysis: 'perTest',
5 mutate: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/**/__tests__/**'],
6 thresholds: { high: 80, low: 60, break: 50 },
7 reporters: ['html', 'clear-text'],
8 timeoutMS: 10000,
9 concurrency: 4,
10};
11
12// Run:
13// npx stryker run
14// npx stryker run --mutate src/pricing.ts
15
16// CI (nightly, not every PR — too slow):
17// schedule:
18// - cron: '0 2 * * *' # 2am daily
19// jobs:
20// mutation:
21// steps:
22// - run: npx stryker run --reporters dashboardThe Stryker HTML report shows each survived mutant with the original and mutated code:
1File: src/utils/pricing.ts
2Mutation score: 73% (22/30 killed)
3
4Survived mutants:
5Line 15 | ArithmeticOperator | return price * (1 - discount) → return price * (1 + discount)
6Line 22 | RelationalOperator | if (qty > 0) → if (qty >= 0)
7Line 31 | StringLiteral | throw Error('Invalid') → throw Error('')1// For each survived mutant, add a test:
2// Line 15 survived: add assertion on return value
3test('calculates correct discounted price', () => {
4 expect(applyDiscount(100, 0.2)).toBe(80); // kills arithmetic mutant
5});
6
7// Line 22 survived: add boundary test
8test('quantity validation: zero is invalid', () => {
9 expect(isValidQuantity(0)).toBe(false); // kills >= mutant
10 expect(isValidQuantity(1)).toBe(true);
11});
12
13// Line 31 survived: assert specific error message
14test('throws specific error message', () => {
15 expect(() => validateCoupon('FAKE')).toThrow('Invalid');
16 // toThrow('') would also pass — use specific message
17});Some mutations are behaviorally identical to the original — these are equivalent mutants and should not be killed.
1// Equivalent mutant example:
2function initCounter() {
3 count = 0; // Stryker mutates to: count = 1
4}
5// If count is never read in tests, both versions behave identically
6// This is an equivalent mutant — safe to ignore
7
8// Ignore annotation:
9function initCounter() {
10 // Stryker:disable-next-line:ArithmeticOperator
11 count = 0; // No arithmetic mutations applied to this line
12}
13
14// Ignore a block:
15// Stryker:disable ArithmeticOperator,StringLiteral
16function legacyCode() {
17 return 'legacy string' + (x + y);
18}
19// Stryker:enable ArithmeticOperator,StringLiteral
20
21// Document why you're ignoring:
22// Stryker:disable-next-line:RelationalOperator -- Arrays cannot have negative length, equivalent mutant
23if (items.length < 0) { /* unreachable */ }Stryker configuration and mutation-killing tests for a business-critical pricing utility.
1// Reading Stryker HTML report
2// Mutation score = (killed / total) * 100
3// Open: reports/mutation/html/index.html
4
5// Example file report:
6// src/pricing.ts Mutation score: 73% (22 killed, 8 survived)
7//
8// Survived mutants:
9// Line 15: return price * (1 + discountPct / 100) [ArithmeticOperator]
10// Line 22: if (items.length < 0) [RelationalOperator]
11// Line 31: throw new Error('') [StringLiteral]
12
13// How to use the report:
14const SURVIVED_MUTANT_PRIORITIES = [
15 'Check if the mutation changes observable behavior',
16 'If yes: add a test that catches this specific mutation',
17 'If no (equivalent mutant): add a @stryker-mutate-ignore comment',
18 'Common equivalent mutants: incrementing unused counters',
19];
20
21// Example: killing a survived relational mutant:
22// Original code (line 22): if (items.length < 0)
23// Survived mutation: if (items.length <= 0) — identical behavior for arrays!
24// This IS an equivalent mutant — arrays cannot have negative length
25// Solution: fix the code to if (items.length === 0) — clear intent, no equivalence
26
27// Example: killing a string literal mutant:
28// Original: throw new Error('Invalid coupon code')
29// Survived: throw new Error('')
30// Fix: test the error message too:
31test('throws with specific error message', () => {
32 expect(() => applyCoupon(100, 'FAKE')).toThrow('Invalid coupon code');
33 // NOT: expect(() => applyCoupon(100, 'FAKE')).toThrow(); — too loose
34});This example demonstrates reading and interpreting mutation reports applied to a pricing utility — one of the highest-value mutation testing targets. The tests are written specifically to kill known mutation types: boundary values for relational operators, exact return value assertions for arithmetic operators, and specific error message assertions for string literal mutations. The mutation score improves from 60% (with coverage-only tests) to 95%+ (with mutation-killing tests). Run: npx stryker run --mutate src/utils/pricing.ts
Continue with the next mutation testing topic to build comprehensive quality verification for your critical business logic.
Your progress is saved in this browser, on this device only.