Mutation Testing — Testing Your Tests (2026)
In this tutorial, you'll learn about Mutation Testing. We cover key concepts, practical examples, and best practices.
Mutation testing evaluates the quality of your tests by introducing small changes (mutations) to your source code and checking whether your tests detect and fail on those changes.
What You'll Learn
You'll understand how mutation testing works, use the Stryker framework to measure mutant survival rates, interpret mutation scores to gain code quality insights, and integrate mutation testing into your CI pipeline.
Why Mutation Testing Matters
Code coverage tells you what code runs during tests — but not whether the tests actually verify the behavior. A function can have 100% line coverage with zero meaningful assertions. Mutation testing answers the real question: "If I introduced a bug, would my tests catch it?" At DodaTech, Durga Antivirus Pro uses mutation testing to validate that signature matching tests actually detect incorrect patterns.
Mutation Testing Learning Path
flowchart LR A[Code Coverage] --> B[Mutation Testing] B --> C[Stryker Framework] B --> D[Quality Gates in CI] style B fill:#f90,color:#fff
How Mutation Testing Works
flowchart LR
A[Original Code] --> B[Run Tests]
B --> C{All tests pass?}
C -->|Yes| D[Create Mutants]
D --> E[Run tests on mutant]
E --> F{Mutant killed?}
F -->|Yes| G[Good test]
F -->|No| H[Surviving mutant]
H --> I[Weak or missing test]
Mutation testing follows these steps:
- Run the original tests — ensure the suite passes
- Create mutants — apply small code changes (mutations)
- Test each mutant — run tests against the mutated code
- Evaluate — if tests fail, the mutant is "killed." If tests pass, the mutant "survived."
Types of Mutations
| Mutation | Original | Mutated | What It Tests |
|---|---|---|---|
| Constant replacement | if (age >= 18) |
if (age >= 17) |
Boundary checks |
| Arithmetic operator | a + b |
a - b |
Math logic |
| Boolean negation | if (isActive) |
if (!isActive) |
Conditional logic |
| Conditional boundary | > becomes >= |
>= becomes > |
Edge cases |
| Null/empty return | return value |
return null |
Null handling |
Stryker Framework
Stryker is the leading mutation testing framework for JavaScript and TypeScript.
Setup
npm install --save-dev @stryker-mutator/core
// stryker.config.json
{
"$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"packageManager": "npm",
"reporters": ["html", "clear-text", "progress"],
"testRunner": "jest",
"coverageAnalysis": "perTest",
"thresholds": {
"high": 80,
"low": 60,
"break": 50
}
}
Running Stryker
npx stryker run
Expected output:
Mutant(s) killed: 42
Mutant(s) survived: 8
Mutation score: 84.0% (42/50)
Interpreting Results
# ----------|---------|----------|-----------|------------|----------|----------|
# File | % score | # killed | # survived | # timeout | # no cov | # errors |
# ----------|---------|----------|-----------|------------|----------|----------|
# math.js | 100.00 | 5 | 0 | 0 | 0 | 0 |
# user.js | 66.67 | 4 | 2 | 0 | 0 | 0 |
# utils.js | 50.00 | 3 | 3 | 0 | 0 | 0 |
# ----------|---------|----------|-----------|------------|----------|----------|
# All files | 84.00 | 42 | 8 | 0 | 0 | 0 |
Files with low scores indicate weak tests. Surviving mutants highlight specific missing assertions.
What Surviving Mutants Tell You
// Original function
function isEven(n) {
return n % 2 === 0;
}
// Mutation: change === to !==
function isEven_Mutated(n) {
return n % 2 !== 0; // Mutant
}
// Test — does this catch the mutation?
test('returns true for even numbers', () => {
expect(isEven(2)).toBe(true);
});
// This test PASSES on the mutant too!
// The mutant SURVIVES — meaning the test doesn't verify strongly enough.
A better test would check both even and odd:
test('returns true for even numbers', () => {
expect(isEven(2)).toBe(true);
expect(isEven(4)).toBe(true);
});
test('returns false for odd numbers', () => {
expect(isEven(1)).toBe(false);
expect(isEven(3)).toBe(false);
});
// Now the mutant is killed — tests fail when === becomes !==
Mutation Score Targets
| Score Range | Meaning | Action |
|---|---|---|
| 90-100% | Excellent | Maintain current practices |
| 70-89% | Good | Review surviving mutants |
| 50-69% | Needs improvement | Add assertions for weak areas |
| Below 50% | Poor | Fundamental test gaps exist |
Integrating Mutation Testing into CI
# .github/workflows/mutation.yml
name: Mutation Testing
on: [pull_request]
jobs:
stryker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx stryker run
env:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
- name: Check mutation score
run: |
SCORE=$(npx stryker run --silent | grep 'Mutation score' | awk '{print $3}')
if (( $(echo "$SCORE < 60" | bc -l) )); then
echo "Mutation score $SCORE% below 60% threshold"
exit 1
fi
Best Practices
1. Start with Critical Modules
Don't run mutation testing on every file. Start with core business logic and security-critical code.
2. Set Incremental Goals
Improve mutation score gradually: 50% → 60% → 70% → 80%.
3. Review Surviving Mutants
Each surviving mutant represents a potential blind spot. Investigate and fix.
4. Combine with Coverage
High coverage + low mutation score = weak assertions. Low coverage + any mutation score = untested code.
5. Run Selectively in CI
Mutation testing is slow (minutes to hours). Run on changed files in CI, full run before releases.
6. Use Mutant Filtering
Exclude generated code, third-party integrations, and trivial getters/setters.
{
"mutate": ["src/**/*.js", "!src/**/*.test.js", "!src/vendor/**"]
}
Common Mistakes
1. Expecting 100% Score Immediately
Mutation testing is strict. 80-90% is excellent for most projects.
2. Ignoring Surviving Mutants
Every surviving mutant is a vulnerability in your test suite. Investigate each one.
3. Running on Everything
Mutation testing is computationally expensive. Target critical modules first.
4. Not Updating Tests for Found Gaps
Running mutation testing without fixing found issues is wasted effort.
5. Confusing Coverage with Mutation Score
90% coverage ≠ 90% mutation score. Coverage measures execution; mutation measures detection.
6. No Baseline
Run mutation testing once to establish a baseline, then track improvements over time.
7. Not Automating
Manual mutation testing is impractical. Automate it in CI.
Practice Questions
1. What is mutation testing? A technique that introduces small changes (mutations) to source code and checks whether tests detect and fail on those changes.
2. What is a surviving mutant? A code change that tests didn't detect — meaning the tests don't verify that specific behavior.
3. What is a good mutation score target? 80%+ for critical modules, 70%+ for the overall project. 90%+ is excellent.
4. How is mutation testing different from code coverage? Coverage measures which code runs during tests. Mutation measures whether tests would catch bugs in that code.
5. Challenge: Improve mutation score for a discount calculator. Take a function with 100% coverage but surviving mutants. Add assertions to kill the mutants.
Mini Project: Mutation Score Tracker
// mutation-tracker.js
class MutationTracker {
constructor() {
this.runs = [];
}
addRun(moduleName, killed, survived) {
const total = killed + survived;
this.runs.push({
module: moduleName,
date: new Date().toISOString(),
killed,
survived,
total,
score: total > 0 ? Math.round((killed / total) * 100) : 100,
});
}
getTrend(moduleName) {
return this.runs
.filter(r => r.module === moduleName)
.sort((a, b) => new Date(a.date) - new Date(b.date));
}
getLowestScore() {
return this.runs.reduce(
(min, r) => r.score < min.score ? r : min,
this.runs[0]
);
}
generateReport() {
const latest = this.runs[this.runs.length - 1];
return {
latestScore: latest?.score ?? 'No runs yet',
modulesTested: new Set(this.runs.map(r => r.module)).size,
totalRuns: this.runs.length,
lowestScore: this.getLowestScore(),
recommendation: latest?.score < 70
? 'Review surviving mutants in critical modules'
: 'Mutation coverage looks healthy',
};
}
}
const tracker = new MutationTracker();
tracker.addRun('math.js', 15, 0);
tracker.addRun('user.js', 22, 5);
tracker.addRun('payment.js', 8, 8);
console.log(tracker.generateReport());
Expected output:
{
latestScore: 50,
modulesTested: 3,
totalRuns: 3,
lowestScore: { module: 'payment.js', score: 50, killed: 8, survived: 8, ... },
recommendation: 'Review surviving mutants in critical modules'
}
FAQ
What's Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro