Strategies to Kill Surviving Mutants — using mutation testing to verify test quality beyond code coverage.
Strategies to Kill Surviving Mutants 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// Strategies to kill surviving mutants
2// Strategy 1: Boundary value testing kills relational mutants
3function isEligible(age: number): boolean {
4 return age >= 18;
5}
6
7// Kills: age > 18, age <= 18, age === 18
8test('boundary values kill relational mutants', () => {
9 expect(isEligible(17)).toBe(false); // kills: age > 18 (17 > 18 = false, correct)
10 expect(isEligible(18)).toBe(true); // kills: age > 18 (18 > 18 = false, wrong!)
11 expect(isEligible(19)).toBe(true); // additional coverage
12});
13
14// Strategy 2: Assert exact return values kill arithmetic mutants
15function calculateTax(price: number, rate: number): number {
16 return price * rate;
17}
18// BAD test: just checks it doesn't throw
19test('calculateTax runs', () => { calculateTax(100, 0.2); });
20// GOOD test: checks exact value
21test('calculateTax returns correct value', () => {
22 expect(calculateTax(100, 0.2)).toBe(20); // kills: price / rate, price + rate, etc.
23 expect(calculateTax(50, 0.1)).toBe(5); // kills: surviving constant mutants
24});
25
26// Strategy 3: Test error messages kill string literal mutants
27test('error message is specific', () => {
28 expect(() => divide(1, 0)).toThrow('Division by zero');
29 // Not: .toThrow() — too loose, survives StringLiteral mutation
30});
31
32// Strategy 4: Add @stryker-mutate-ignore for equivalent mutants
33function resetCounter(): void {
34 count = 0; // mutated to: count = 1;
35 // BUT: counter is never read in tests — equivalent mutant
36 // Stryker ignore comment:
37 /* istanbul ignore next -- @stryker-mutate-ignore */
38}
39
40// Focus on high-value code:
41const MUTATION_PRIORITIES = [
42 'Pricing/discount logic',
43 'Authentication/authorization checks',
44 'Permission boundary conditions',
45 'Data validation rules',
46 'Payment calculations',
47];This example demonstrates strategies to kill surviving mutants 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.