| name | testing/mutation-testing |
| description | Mutation testing with Stryker to verify test quality by introducing code mutations and measuring detection rates |
| category | testing |
| tags | ["testing","mutation","stryker","quality","coverage"] |
| related_skills | ["testing/comprehensive-testing","testing/vitest","methodology/quality-gates"] |
Mutation Testing
Measure test quality by introducing bugs (mutations) and verifying your tests catch them.
Quick Start
npm install -D @stryker-mutator/core @stryker-mutator/vitest-runner
npx stryker init
npm run test:mutation
Core Concept
Mutation testing introduces small changes (mutations) to your code and runs your tests. If tests still pass, they're too weak.
function isAdult(age) {
return age >= 18;
}
function isAdult(age) { return age > 18; }
function isAdult(age) { return age <= 18; }
function isAdult(age) { return false; }
function isAdult(age) { return true; }
A good test catches all mutations:
describe('isAdult', () => {
it('returns true for age 18', () => {
expect(isAdult(18)).toBe(true);
});
it('returns false for age 17', () => {
expect(isAdult(17)).toBe(false);
});
it('returns true for age 100', () => {
expect(isAdult(100)).toBe(true);
});
});
Configuration
stryker.config.json
{
"$schema": "https://raw.githubusercontent.com/stryker-mutator/stryker/master/packages/core/schema/stryker-schema.json",
"packageManager": "npm",
"testRunner": "vitest",
"mutate": [
"src/**/*.js",
"src/**/*.ts",
"!src/**/*.test.js",
"!src/**/*.spec.ts"
],
"reporters": [
"progress",
"clear-text",
"html",
"json"
],
"htmlReporter": {
"fileName": "reports/mutation/index.html"
},
"thresholds": {
"high": 80,
"low": 60
Mutation Operators
Arithmetic Operators
a - b
a * b
a / b
Comparison Operators
a >= b
a < b
a <= b
a == b
Logical Operators
a || b
a
Boundary Mutations
i <= 10
i < 11
i < 9
Return Value Mutations
return undefined;
return !value;
return "";
return 0;
return null;
Understanding Results
Mutation States
| State | Description | Action |
|---|
| Killed | Test failed = mutation caught | Good! |
| Survived | Tests passed = mutation missed | Add tests |
| Timeout | Tests took too long | Check infinite loops |
| No Coverage | No tests cover this code | Add tests |
| Compile Error | Mutation broke compilation | Ignore |
Mutation Score
Mutation Score = (Killed / Total) * 100%
- 80%+: Excellent test quality
- 60-80%: Good, room for improvement
- 40-60%: Weak tests, many gaps
- <40%: Critical test deficiency
Improving Mutation Score
1. Boundary Testing
it('validates age', () => {
expect(isValidAge(25)).toBe(true);
});
it('validates age boundaries', () => {
expect(isValidAge(0)).toBe(true);
expect(isValidAge(-1)).toBe(false);
expect(isValidAge(150)).toBe(true);
expect(isValidAge(151)).toBe(false);
});
2. Condition Coverage
function process(a, b) {
if (a > 0 && b > 0) {
return 'both positive';
}
return 'not both positive';
}
it('processes positive', () => {
expect(process(1, 1)).toBe('both positive');
});
it('processes various combinations', () => {
expect(process(1, 1)).toBe('both positive');
expect(process(-1, 1)).toBe('not both positive');
expect(process(1, -1)).toBe('not both positive');
expect(process(0, 1)).toBe('not both positive');
});
3. Return Value Testing
it('checks admin', () => {
expect(isAdmin(adminUser)).toBeTruthy();
});
it('checks admin status', () => {
expect(isAdmin(adminUser)).toBe(true);
expect(isAdmin(regularUser)).toBe(false);
expect(isAdmin(null)).toBe(false);
});
Incremental Mutation Testing
For large codebases, run mutations on changed files only:
git diff --name-only origin/main | xargs npx stryker run --mutate
CI Integration
GitHub Actions
name: Mutation Testing
on:
push:
branches: [main]
pull_request:
jobs:
mutation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Run Mutation Tests
run: npm run test:mutation
- name: Upload Report
uses: actions/upload-artifact@v4
with:
name: mutation-report
path: reports/mutation/
Performance Optimization
Reduce Mutation Scope
{
"mutate": [
"src/core/**/*.js",
"!src/core/**/*.test.js",
"!src/core/generated/**"
]
}
Increase Parallelism
{
"concurrency": 8,
"testRunner": "vitest"
}
Filter Mutators
{
"mutator": {
"excludedMutations": [
"StringLiteral",
"ObjectLiteral"
]
}
}
When to Use
Good Candidates
- Critical business logic
- Security-sensitive code
- Mathematical calculations
- State machines
- Validation logic
When to Skip
- Generated code
- Configuration files
- Third-party wrappers
- UI components
- Test utilities
Anti-Patterns
- Chasing 100%: Diminishing returns above 90%
- Ignoring Timeouts: Fix infinite loop mutations
- Testing Everything: Focus on critical paths
- No Baseline: Establish baseline before improving
- Infrequent Runs: Run on every PR