Navigate Stryker mutation reports to find survived mutants, distinguish equivalent mutants from genuine test gaps, and write targeted tests that kill specific mutation types.
After running Stryker Mutator, you receive an HTML report showing every mutant that survived your test suite. A survived mutant represents a bug your tests cannot detect — a genuine gap in test quality. Reading these reports efficiently and prioritizing which survived mutants to kill is the core skill of mutation testing in practice.
This lesson covers the complete report review workflow: understanding mutant statuses, finding high-value survived mutants, identifying equivalent mutants, and writing tests that precisely kill each mutation type.
The Stryker HTML report (reports/mutation/html/index.html) shows mutation results at file, function, and line level.
1# Generate and open the report:
2npx stryker run
3open reports/mutation/html/index.html # macOS
4start reports/mutation/html/index.html # Windows
5
6# Report sections:
7# Overview: overall mutation score bar chart
8# File tree: per-file scores (red = poor, green = good)
9# File view: line-by-line mutant status with diffsMutant status meanings:
Priority order for fixing:
Equivalent mutants change code but produce identical program behavior — they cannot be killed by any test and should be suppressed with documentation.
1// Example 1: Counter never observed in tests
2class RequestLogger {
3 private count = 0;
4 log(msg: string) {
5 this.count++; // Stryker:disable-next-line:ArithmeticOperator
6 // ^ count is incremented but never read in tests — equivalent mutant
7 console.error('[LOG]', msg);
8 }
9}
10
11// Example 2: Boundary on logically impossible value
12function processItems(items: unknown[]): void {
13 // Stryker:disable-next-line:RelationalOperator
14 // ^ arrays cannot have negative length — items.length < 0 is always false
15 if (items.length < 0) throw new Error('impossible negative length');
16 for (const item of items) { /* process */ }
17}
18
19// Example 3: String that is never compared in tests
20function getErrorCode(type: string): string {
21 if (type === 'auth') return 'AUTH_001';
22 return 'UNKNOWN'; // Stryker:disable-next-line:StringLiteral
23 // ^ If UNKNOWN is never asserted in tests, the mutant ('') survives
24 // Either: add test asserting 'UNKNOWN', or annotate as equivalent
25}
26
27// Rule: before annotating, ask 'Is there any observable difference?'
28// If yes: write a test. If no: annotate with explanation.Relational operators (<, >, <=, >=, ===, !==) are mutated to all other relational forms. Boundary value tests kill all variations.
1// Function under test:
2function canAccess(userAge: number, requiredAge: number): boolean {
3 return userAge >= requiredAge;
4}
5
6// Stryker mutations:
7// userAge >= requiredAge → userAge > requiredAge [SURVIVED]
8// userAge >= requiredAge → userAge <= requiredAge [SURVIVED]
9// userAge >= requiredAge → userAge < requiredAge [SURVIVED]
10// userAge >= requiredAge → userAge === requiredAge [SURVIVED]
11
12// These survive because tests only checked values far from the boundary.
13// Kill ALL relational mutations with three boundary tests:
14describe('canAccess — kills relational mutants', () => {
15 it('returns true at exact boundary (kills >)', () => {
16 expect(canAccess(18, 18)).toBe(true);
17 // If mutated to >: canAccess(18, 18) = false ≠ true → KILLED
18 });
19
20 it('returns false just below boundary (kills >=)', () => {
21 expect(canAccess(17, 18)).toBe(false);
22 // If mutated to <=: canAccess(17, 18) = true ≠ false → KILLED
23 });
24
25 it('returns true above boundary (basic sanity check)', () => {
26 expect(canAccess(19, 18)).toBe(true);
27 });
28});
29// These 3 tests kill all 5 relational mutation variants1// Function under test:
2export function applyTax(price: number, taxRate: number): number {
3 return price * (1 + taxRate);
4}
5
6// Stryker mutations:
7// price * (1 + taxRate) → price * (1 - taxRate) [SURVIVED]
8// price + (1 + taxRate) — unlikely but possible [SURVIVED]
9
10// Kill with exact assertion using non-trivial values:
11describe('applyTax — kills arithmetic mutants', () => {
12 it('returns correct value for 20% tax', () => {
13 expect(applyTax(100, 0.2)).toBeCloseTo(120);
14 // If 1 + 0.2 mutated to 1 - 0.2: 100 * 0.8 = 80 ≠ 120 → KILLED
15 });
16
17 it('returns price unchanged for 0% tax', () => {
18 expect(applyTax(100, 0)).toBe(100); // base case
19 });
20});
21
22// Killing string literal mutants:
23export function formatError(code: string): string {
24 return 'Error [' + code + ']: Operation failed';
25}
26// Mutation: 'Error [' → '' — SURVIVED if tests never check format
27describe('formatError — kills string literal mutants', () => {
28 it('includes error code in formatted message', () => {
29 const msg = formatError('AUTH_001');
30 expect(msg).toContain('AUTH_001');
31 expect(msg).toContain('Error');
32 expect(msg).toContain('Operation failed');
33 // All three: if any part is mutated to '', test fails
34 });
35});1// stryker.config.mjs — enable dashboard reporting
2export default {
3 reporters: ['html', 'clear-text', 'dashboard'],
4 dashboard: {
5 project: 'github.com/your-org/developerbootstrap',
6 version: process.env.GIT_BRANCH || 'main',
7 },
8 thresholds: {
9 high: 80, // green score badge
10 low: 60, // yellow score badge
11 break: 50, // CI failure threshold
12 },
13};
14
15// GitHub Actions weekly mutation run:
16// .github/workflows/mutation.yml
17// schedule:
18// - cron: '0 3 * * 1' # Monday 3am UTC
19// steps:
20// - run: npx stryker run
21// env:
22// STRYKER_DASHBOARD_API_KEY: secrets.STRYKER_DASHBOARD_API_KEY
23// GIT_BRANCH: github.ref_name
24
25// dashboard.stryker-mutator.io shows:
26// - Score history as a line chart
27// - Breakdown by mutation type
28// - Comparison between branches
29// - Badge URL for README: shows current mutation score
30
31console.log('View dashboard: https://dashboard.stryker-mutator.io');Complete walkthrough of reading a Stryker report for a pricing utility and writing tests that kill all 6 survived mutants.
1// Pricing utility with survived mutants
2export function calculateFinalPrice(
3 basePrice: number,
4 discountPct: number,
5 taxRate: number,
6 isStudent: boolean
7): number {
8 if (basePrice <= 0) throw new RangeError('Price must be positive');
9 if (discountPct < 0 || discountPct > 100) throw new RangeError('Discount 0-100');
10 const discountedPrice = basePrice * (1 - discountPct / 100);
11 const studentDiscount = isStudent ? discountedPrice * 0.9 : discountedPrice;
12 return studentDiscount * (1 + taxRate);
13}
14
15// Stryker report: 6 survived mutations
16// 1. basePrice <= 0 → basePrice < 0 [RelationalOperator] SURVIVED
17// 2. discountPct < 0 → discountPct <= 0 [RelationalOperator] SURVIVED
18// 3. discountPct > 100 → discountPct >= 100 [RelationalOperator] SURVIVED
19// 4. 1 - discountPct → 1 + discountPct [ArithmeticOperator] SURVIVED
20// 5. discountedPrice * 0.9 → * 0.1 [ArithmeticOperator] SURVIVED
21// 6. 1 + taxRate → 1 - taxRate [ArithmeticOperator] SURVIVED
22
23describe('calculateFinalPrice — mutation-killing tests', () => {
24 // Kill mutant 1: <= 0 vs < 0 (price = 0 must throw)
25 test('throws on zero price', () => {
26 expect(() => calculateFinalPrice(0, 0, 0.2, false)).toThrow(RangeError);
27 expect(() => calculateFinalPrice(0.01, 0, 0.2, false)).not.toThrow();
28 // If mutated to < 0: calculateFinalPrice(0,...) would NOT throw — test catches it
29 });
30
31 // Kill mutant 2: < 0 vs <= 0 (discount = 0 is valid)
32 test('allows zero percent discount', () => {
33 expect(() => calculateFinalPrice(100, 0, 0.2, false)).not.toThrow();
34 expect(() => calculateFinalPrice(100, -0.001, 0.2, false)).toThrow();
35 });
36
37 // Kill mutant 3: > 100 vs >= 100 (discount = 100 is valid)
38 test('allows one hundred percent discount', () => {
39 expect(() => calculateFinalPrice(100, 100, 0.2, false)).not.toThrow();
40 expect(calculateFinalPrice(100, 100, 0.2, false)).toBe(0);
41 expect(() => calculateFinalPrice(100, 100.001, 0.2, false)).toThrow();
42 });
43
44 // Kill mutants 4, 5, 6: arithmetic operators
45 test('calculates correct final price for all scenarios', () => {
46 // 100, 20% off, 20% tax, no student: 100 * 0.8 * 1.2 = 96
47 expect(calculateFinalPrice(100, 20, 0.2, false)).toBeCloseTo(96);
48 // Kills mutant 4: 1 + 0.2 = 1.2 ≠ 0.8 (1 - 0.2)
49
50 // 100, 0% off, 20% tax, student: 100 * 1 * 0.9 * 1.2 = 108
51 expect(calculateFinalPrice(100, 0, 0.2, true)).toBeCloseTo(108);
52 // Kills mutant 5: 0.9 ≠ 0.1 → 100 * 0.1 * 1.2 = 12 ≠ 108
53
54 // 100, 0% off, 10% tax, no student: 100 * 1 * 1.1 = 110
55 expect(calculateFinalPrice(100, 0, 0.1, false)).toBeCloseTo(110);
56 // Kills mutant 6: 1 - 0.1 = 0.9 → 100 * 0.9 = 90 ≠ 110
57 });
58
59 // Result:
60 // Before: mutation score 40% (4/10 killed)
61 // After: mutation score 100% (10/10 killed)
62});This case study shows the complete mutation report review → test writing → verification cycle. Each survived mutant maps to a specific test: boundary tests for relational operators (zero price, zero discount, 100% discount), exact value assertions for arithmetic operators (asserting 96, 108, and 110 — values that would be different for every arithmetic mutation). After adding these 4 tests, mutation score improves from 40% to 100% for this file. Run: npx stryker run --mutate apps/api/src/utils/pricing.ts to verify.
The next lesson covers strategies to improve mutation scores systematically — applying mutation testing cost-effectively to high-value business logic while managing CI run time.
Your progress is saved in this browser, on this device only.