| created | "2025-12-16T00:00:00.000Z" |
| modified | "2025-12-16T00:00:00.000Z" |
| reviewed | "2025-12-16T00:00:00.000Z" |
| name | mutation-testing |
| description | Validate test effectiveness with mutation testing using Stryker (TypeScript/JavaScript)
and mutmut (Python). Find weak tests that pass despite code mutations.
Use when user mentions mutation testing, Stryker, mutmut, test effectiveness,
finding weak tests, or improving test quality through mutation analysis.
|
| allowed-tools | Bash, Read, Edit, Write, Grep, Glob, TodoWrite |
Mutation Testing
Expert knowledge for mutation testing - validating that your tests actually catch bugs by introducing deliberate code mutations.
Core Expertise
Mutation Testing Concept
- Mutants: Small code changes (mutations) introduced automatically
- Killed: Test fails with mutation (good - test caught the bug)
- Survived: Test passes with mutation (bad - weak test)
- Coverage: Tests execute mutated code but don't catch it
- Score: Percentage of mutants killed (aim for 80%+)
What Mutation Testing Reveals
- Tests that don't actually verify behavior
- Missing assertions or edge cases
- Overly permissive assertions
- Dead code or unnecessary logic
- Areas needing stronger tests
TypeScript/JavaScript (Stryker)
Installation
bun add -d @stryker-mutator/core
npm install -D @stryker-mutator/core
bun add -d @stryker-mutator/vitest-runner
bun add -d @stryker-mutator/jest-runner
Configuration
export default {
packageManager: 'bun',
reporters: ['html', 'clear-text', 'progress', 'dashboard'],
testRunner: 'vitest',
coverageAnalysis: 'perTest',
mutate: [
'src/**/*.ts',
'!src/**/*.test.ts',
'!src/**/*.spec.ts',
'!src/**/*.d.ts',
],
thresholds: {
high: 80,
low: 60,
break: 60,
},
incremental: true,
incrementalFile: '.stryker-tmp/incremental.json',
concurrency: 4,
timeoutMS: 60000,
}
Running Stryker
npx stryker run
npx stryker run --incremental
npx stryker run --mutate "src/utils/**/*.ts"
npx stryker run --configFile stryker.prod.config.mjs
npx stryker run --reporters html,clear-text
open reports/mutation/html/index.html
Understanding Results
Mutation score: 82.5%
- Killed: 66 (tests caught the mutation)
- Survived: 14 (tests passed despite mutation - weak tests!)
- No Coverage: 0 (mutated code not executed)
- Timeout: 0 (tests took too long)
- Runtime Errors: 0 (mutation broke the code)
- Compile Errors: 0 (mutation caused syntax error)
Example: Weak Test
function calculateDiscount(price: number, percentage: number): number {
return price - (price * percentage / 100)
}
test('applies discount', () => {
const result = calculateDiscount(100, 10)
expect(result).toBeDefined()
})
test('applies discount correctly', () => {
expect(calculateDiscount(100, 10)).toBe(90)
expect(calculateDiscount(100, 20)).toBe(80)
expect(calculateDiscount(50, 10)).toBe(45)
})
Common Mutation Types
Ignoring Specific Mutations
function debugOnlyCode() {
console.log('This won\'t be mutated')
}
const total = price + tax
CI/CD Integration
name: Mutation Testing
on:
pull_request:
branches: [main]
jobs:
mutation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Run mutation tests
run: bun run stryker run
- name: Upload mutation report
uses: actions/upload-artifact@v3
if: always()
with:
name: mutation-report
path: reports/mutation/html/
Python (mutmut)
Installation
uv add --dev mutmut
pip install mutmut
Basic Configuration
[tool.mutmut]
paths_to_mutate = "src/"
backup = false
runner = "python -m pytest -x --assert=plain -q"
tests_dir = "tests/"
Running mutmut
uv run mutmut run
uv run mutmut run --paths-to-mutate=src/calculator.py
uv run mutmut results
uv run mutmut summary
uv run mutmut show 1
uv run mutmut apply 1
uv run mutmut html
open html/index.html
Understanding Results
Status: 45/50 mutants killed (90%)
- Killed: 45 (tests caught the mutation)
- Survived: 5 (tests passed despite mutation)
- Suspicious: 0 (inconsistent results)
- Timeout: 0 (tests took too long)
Example: Improving Weak Test
def calculate_discount(price: float, percentage: float) -> float:
return price - (price * percentage / 100)
def test_applies_discount():
result = calculate_discount(100, 10)
assert result is not None
def test_applies_discount_correctly():
assert calculate_discount(100, 10) == 90.0
assert calculate_discount(100, 20) == 80.0
assert calculate_discount(50, 10) == 45.0
assert calculate_discount(0, 10) == 0.0
Common Mutation Types (Python)
Filtering Results
uv run mutmut results | grep "^Survived"
uv run mutmut results | grep "^Killed"
uv run mutmut show src/calculator.py
Excluding Code from Mutation
def legacy_code():
pass
result = expensive_computation()
CI/CD Integration
name: Mutation Testing
on:
pull_request:
branches: [main]
jobs:
mutation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v1
- name: Install dependencies
run: uv sync
- name: Run mutation tests
run: uv run mutmut run
- name: Show results
if: always()
run: uv run mutmut results
- name: Generate HTML report
if: always()
run: uv run mutmut html
- name: Upload report
uses: actions/upload-artifact@v3
if: always()
with:
name: mutation-report
path: html/
Interpreting Results
Mutation Score Targets
| Score | Quality | Action |
|---|
| 90%+ | Excellent | Maintain quality |
| 80-89% | Good | Small improvements |
| 70-79% | Acceptable | Focus on weak areas |
| 60-69% | Needs work | Add missing tests |
| < 60% | Poor | Major test improvements needed |
Survived Mutants Analysis
Common Reasons Mutants Survive:
- Missing assertions
test('processes data', () => {
processData(input)
})
test('processes data', () => {
const result = processData(input)
expect(result).toEqual(expected)
})
- Weak assertions
def test_calculation():
result = calculate(10, 5)
assert result > 0
def test_calculation():
result = calculate(10, 5)
assert result == 15
- Dead code
function example(x: number) {
if (x > 10) {
return 'large'
}
if (x < 0) {
return 'negative'
}
return 'small'
}
test('handles edge case', () => {
expect(example(0)).toBe('small')
})
- Equivalent mutants
const result = x + 0
const result = x - 0
Best Practices
When to Run Mutation Tests
- After achieving high code coverage (80%+)
- Before major releases
- On critical business logic modules
- When test quality is questioned
- In CI for important branches
Incremental Approach
- Start with core business logic modules
- Fix survived mutants
- Gradually expand coverage
- Integrate into CI pipeline
- Maintain high mutation score
Performance Optimization
export default {
incremental: true,
mutate: ['src/core/**/*.ts', 'src/utils/**/*.ts'],
disableTypeChecks: '{src,test}/**/*.ts',
concurrency: 4,
}
uv run mutmut run --paths-to-mutate=src/core/
Common Pitfalls
- ❌ Running mutation tests before basic coverage
- ❌ Expecting 100% mutation score
- ❌ Not excluding generated code
- ❌ Treating equivalent mutants as failures
- ❌ Running full mutation suite on every commit
Integration with Coverage
vitest --coverage
npx stryker run
Workflow Example
TypeScript/Vitest/Stryker
bun test --coverage
open coverage/index.html
npx stryker run
open reports/mutation/html/index.html
npx stryker run --incremental
Python/pytest/mutmut
uv run pytest --cov
uv run pytest --cov --cov-report=html
open htmlcov/index.html
uv run mutmut run
uv run mutmut results
uv run mutmut show <id>
uv run mutmut run --paths-to-mutate=src/fixed_module.py
Improving Weak Tests
Pattern: Insufficient Assertions
test('calculates sum', () => {
const result = sum([1, 2, 3])
expect(result).toBeGreaterThan(0)
})
test('calculates sum correctly', () => {
expect(sum([1, 2, 3])).toBe(6)
expect(sum([0, 0, 0])).toBe(0)
expect(sum([-1, 1])).toBe(0)
expect(sum([])).toBe(0)
})
Pattern: Missing Edge Cases
def test_divide():
result = divide(10, 2)
assert result == 5
def test_divide():
assert divide(10, 2) == 5
assert divide(9, 3) == 3
assert divide(7, 2) == 3.5
with pytest.raises(ZeroDivisionError):
divide(10, 0)
Pattern: Boundary Conditions
test('validates age', () => {
expect(isValidAge(25)).toBe(true)
})
test('validates age boundaries', () => {
expect(isValidAge(18)).toBe(true)
expect(isValidAge(17)).toBe(false)
expect(isValidAge(100)).toBe(true)
expect(isValidAge(101)).toBe(false)
expect(isValidAge(0)).toBe(false)
expect(isValidAge(-1)).toBe(false)
})
Troubleshooting
Mutation tests taking too long
export default {
concurrency: Math.max(os.cpus().length - 1, 1),
timeoutMS: 30000,
}
uv run mutmut run --use-coverage
Too many survived mutants
- Focus on critical modules first
- Add specific assertions
- Test boundary conditions
- Review mutation details
All tests failing
bun test
False positives (equivalent mutants)
const result = x + 0
See Also
vitest-testing - Unit testing framework
python-testing - Python pytest testing
test-quality-analysis - Detecting test smells
api-testing - HTTP API testing
References