Stryker Mutator: Setup, Configuration, and First Run — using mutation testing to verify test quality beyond code coverage.
Stryker Mutator: Setup, Configuration, and First Run 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// stryker.config.mjs — Stryker configuration
2/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
3export default {
4 packageManager: 'npm',
5 reporters: ['html', 'clear-text', 'progress', 'dashboard'],
6 testRunner: 'jest',
7 coverageAnalysis: 'perTest',
8 mutate: [
9 'src/**/*.ts',
10 '!src/**/*.spec.ts',
11 '!src/**/*.d.ts',
12 '!src/**/index.ts',
13 '!src/**/__tests__/**',
14 ],
15 thresholds: {
16 high: 80, // green badge
17 low: 60, // yellow badge
18 break: 50, // fail CI below this score
19 },
20 timeoutMS: 10000,
21 concurrency: 4,
22 jest: {
23 projectType: 'custom',
24 configFile: 'jest.config.ts',
25 enableFindRelatedTests: true,
26 },
27 ignorePatterns: [
28 'node_modules',
29 'dist',
30 'coverage',
31 ],
32};
33
34// Run Stryker:
35// npx stryker run # full run
36// npx stryker run --mutate src/pricing.ts # single file
37// npx stryker run --reporters html # generate HTML report
38
39// CI (slow — run nightly, not on every PR):
40// - name: Mutation testing
41// run: npx stryker run --reporters dashboard
42// env: { STRYKER_DASHBOARD_API_KEY: KEY }
43// if: github.event_name == 'schedule'This example demonstrates stryker mutator: setup, configuration, and first run 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.