| name | mutation-testing |
| description | Mutation testing patterns for verifying test effectiveness. Use when analyzing branch code to find weak or missing tests. |
Mutation Testing
This skill focuses on verifying test effectiveness.
Mutation testing answers the question: "Are my tests actually catching bugs?"
Code coverage tells you what code your tests execute. Mutation testing tells you if your tests would detect changes to that code. A test suite with 100% coverage can still miss 40% of potential bugs.
Core Concept
The Mutation Testing Process:
- Generate mutants: Introduce small bugs (mutations) into production code
- Run tests: Execute your test suite against each mutant
- Evaluate results: If tests fail, the mutant is "killed" (good). If tests pass, the mutant "survived" (bad - your tests missed the bug)
The Insight: A surviving mutant represents a bug your tests wouldn't catch.
When to Use This Skill
Use mutation testing analysis when:
- Reviewing code changes on a branch
- Verifying test effectiveness after TDD
- Identifying weak tests that appear to have coverage
- Finding missing edge case tests
- Validating that refactoring didn't weaken test suite
Integration with TDD:
RED-GREEN-MUTATE-REFACTOR Cycle
┌─────────────────────────────────────────────────┐
│ 1. RED: Write failing test │
│ 2. GREEN: Minimum code to pass │
│ 3. MUTATE: Verify tests catch real bugs ◄── │ ← You are here
│ 4. REFACTOR: Improve structure with confidence │
└─────────────────────────────────────────────────┘
Why MUTATE before REFACTOR: Mutation testing validates test strength before you restructure code. Refactoring with unverified tests means restructuring code whose safety net you haven't checked.
Execution Process
When verifying test effectiveness, actually mutate the code and run the tests. Do not just reason about whether tests would catch mutations — prove it.
Step 1: Identify Changed Code
git diff main...HEAD --name-only | grep -E '\.(ts|js|tsx|jsx)$' | grep -v '\.test\.'
git diff main...HEAD -- src/
Step 2: Apply Mutations and Run Tests
For each changed function/method, work through the mutation operators (see Mutation Operators section below). For each applicable mutation:
- Mutate: Change the production code (e.g., flip
* to /, negate a condition)
- Run: Execute the test suite
- Evaluate: Did a test fail?
- Yes → mutant killed (good). Revert the mutation.
- No → mutant survived (bad). Revert the mutation, then add or strengthen a test.
- Revert: Always restore the original code before the next mutation
Always revert each mutation before applying the next. Never leave mutated code in place.
You do not need to apply every possible mutation to every line. Focus on:
- Changed code on the branch
- Operators most likely to have surviving mutants (see Quick Reference)
- Conditions with boundary values
- Boolean logic with multiple operands
Step 3: Produce a Report
After working through the mutations, produce a summary:
## Mutation Testing Report
### Killed (tests caught the mutation)
- `calculateTotal`: `*` → `/` — killed by "calculates total for multiple items"
- `isEligible`: `>=` → `>` — killed by "returns true at exact boundary"
### Survived (tests DID NOT catch the mutation)
- `applyDiscount`: `>` → `>=` — no test for boundary value at exactly 100
→ **Action**: Add boundary test for discount threshold
### Summary
- Mutations applied: 8
- Killed: 6
- Survived: 2
- Mutation score: 75%
Step 4: Kill Surviving Mutants
Not every surviving mutant warrants a new test. Some mutations produce equivalent behavior, and some boundary cases are low-risk enough that the test would add noise without meaningful protection.
Fix immediately when:
- The mutation represents a realistic bug (wrong operator, inverted condition)
- The surviving mutant is in critical business logic (money, permissions, eligibility)
- The fix is a simple boundary test or stronger assertion
Ask the human when:
- You're unsure whether the mutation represents a real risk
- The test to kill it would be complex or hard to name clearly
- The mutation is in a code path that's also covered by integration/E2E tests
- The surviving mutant feels like an equivalent mutant but you're not certain
Present the mutation, explain why the current tests don't catch it, and let the human decide whether it's worth a new test.
When fixing, follow TDD — write the failing test first, verify it fails against the mutated code, then verify it passes against the original code.
Mutation Operators
Arithmetic Operator Mutations
| Original | Mutated | Test Should Verify |
|---|
a + b | a - b | Addition behavior |
a - b | a + b | Subtraction behavior |
a * b | a / b | Multiplication behavior |
a / b | a * b | Division behavior |
a % b | a * b | Modulo behavior |
Example Analysis:
const calculateTotal = (price: number, quantity: number): number => {
return price * quantity;
};
it('calculates total', () => {
expect(calculateTotal(10, 1)).toBe(10);
});
it('calculates total', () => {
expect(calculateTotal(10, 3)).toBe(30);
});
Conditional Expression Mutations
| Original | Mutated | Test Should Verify |
|---|
a < b | a <= b | Boundary value at equality |
a < b | a >= b | Both sides of condition |
a <= b | a < b | Boundary value at equality |
a <= b | a > b | Both sides of condition |
a > b | a >= b | Boundary value at equality |
a > b | a <= b | Both sides of condition |
a >= b | a > b | Boundary value at equality |
a >= b | a < b | Both sides of condition |
Example Analysis:
const isAdult = (age: number): boolean => {
return age >= 18;
};
it('returns true for adults', () => {
expect(isAdult(25)).toBe(true);
});
it('returns true for exactly 18', () => {
expect(isAdult(18)).toBe(true);
});
Equality Operator Mutations
| Original | Mutated | Test Should Verify |
|---|
a === b | a !== b | Both equal and not equal cases |
a !== b | a === b | Both equal and not equal cases |
a == b | a != b | Both equal and not equal cases |
a != b | a == b | Both equal and not equal cases |
Logical Operator Mutations
| Original | Mutated | Test Should Verify |
|---|
a && b | a || b | Case where one is true, other is false |
a || b | a && b | Case where one is true, other is false |
a ?? b | a && b | Nullish coalescing behavior |
Example Analysis:
const canAccess = (isAdmin: boolean, isOwner: boolean): boolean => {
return isAdmin || isOwner;
};
it('returns true when both conditions met', () => {
expect(canAccess(true, true)).toBe(true);
});
it('returns true when only admin', () => {
expect(canAccess(true, false)).toBe(true);
});
Boolean Literal Mutations
| Original | Mutated | Test Should Verify |
|---|
true | false | Both true and false outcomes |
false | true | Both true and false outcomes |
!(a) | a | Negation is necessary |
Block Statement Mutations
| Original | Mutated | Test Should Verify |
|---|
{ code } | { } | Side effects of the block |
Example Analysis:
const processOrder = (order: Order): void => {
validateOrder(order);
saveOrder(order);
sendConfirmation(order);
};
it('processes order without error', () => {
expect(() => processOrder(order)).not.toThrow();
});
it('saves order to database', () => {
processOrder(order);
expect(mockDatabase.save).toHaveBeenCalledWith(order);
});
String Literal Mutations
| Original | Mutated | Test Should Verify |
|---|
"text" | "" | Non-empty string behavior |
"" | "Stryker was here!" | Empty string behavior |
Array Declaration Mutations
| Original | Mutated | Test Should Verify |
|---|
[1, 2, 3] | [] | Non-empty array behavior |
new Array(1, 2) | new Array() | Array contents matter |
Unary Operator Mutations
| Original | Mutated | Test Should Verify |
|---|
+a | -a | Sign matters |
-a | +a | Sign matters |
++a | --a | Increment vs decrement |
a++ | a-- | Increment vs decrement |
Method Expression Mutations (TypeScript/JavaScript)
| Original | Mutated | Test Should Verify |
|---|
startsWith() | endsWith() | Correct string position |
endsWith() | startsWith() | Correct string position |
toUpperCase() | toLowerCase() | Case transformation |
toLowerCase() | toUpperCase() | Case transformation |
some() | every() | Partial vs full match |
every() | some() | Full vs partial match |
filter() | (removed) | Filtering is necessary |
reverse() | (removed) | Order matters |
sort() | (removed) | Ordering is necessary |
min() | max() | Correct extremum |
max() | min() | Correct extremum |
trim() | trimStart() | Correct trim behavior |
Optional Chaining Mutations
| Original | Mutated | Test Should Verify |
|---|
foo?.bar | foo.bar | Null/undefined handling |
foo?.[i] | foo[i] | Null/undefined handling |
foo?.() | foo() | Null/undefined handling |
Mutant States and Metrics
Mutant States
| State | Meaning | Action |
|---|
| Killed | Test failed when mutant applied | Good - tests are effective |
| Survived | Tests passed with mutant active | Bad - add/strengthen test |
| No Coverage | No test exercises this code | Add behavior test |
| Timeout | Tests timed out (infinite loop) | Counted as detected |
| Equivalent | Mutant produces same behavior | No action - not a real bug |
Metrics
- Mutation Score:
killed / valid * 100 - The higher, the better
- Detected:
killed + timeout
- Undetected:
survived + no coverage
Target Mutation Score
| Score | Quality |
|---|
| < 60% | Weak test suite - significant gaps |
| 60-80% | Moderate - many improvements possible |
| 80-90% | Good - but still gaps to address |
| > 90% | Strong - but watch for equivalent mutants |
Equivalent Mutants
Equivalent mutants produce the same behavior as the original code. They cannot be killed because there is no observable difference.
Common Equivalent Mutant Patterns
Pattern 1: Operations with identity elements
if (whatever) {
number += 0;
} else {
number += 0;
}
Pattern 2: Boundary conditions that don't affect outcome
const max = Math.max(a, b);
const min = Math.min(a, b);
if (a >= b) {
result = 10 ** (max - min);
}
Pattern 3: Dead code paths
if (impossibleCondition) {
doSomething();
}
How to Handle Equivalent Mutants
- Identify: Analyze if mutation truly changes observable behavior
- Document: Note why mutant is equivalent
- Accept: 100% mutation score may not be achievable
- Consider refactoring: Sometimes equivalent mutants indicate unclear code
Branch Analysis Checklist
When analyzing code changes on a branch:
For Each Function/Method Changed:
Red Flags (Likely Surviving Mutants):
Questions to Ask:
- "If I changed this operator, would a test fail?"
- "If I negated this condition, would a test fail?"
- "If I removed this line, would a test fail?"
- "If I returned early here, would a test fail?"
Strengthening Weak Tests
Pattern: Add Boundary Value Tests
it('validates age', () => {
expect(isAdult(25)).toBe(true);
expect(isAdult(10)).toBe(false);
});
it('validates age at boundary', () => {
expect(isAdult(17)).toBe(false);
expect(isAdult(18)).toBe(true);
expect(isAdult(19)).toBe(true);
});
Pattern: Test Both Branches of Conditions
it('returns access result', () => {
expect(canAccess(true, true)).toBe(true);
});
it('grants access when admin', () => {
expect(canAccess(true, false)).toBe(true);
});
it('grants access when owner', () => {
expect(canAccess(false, true)).toBe(true);
});
it('denies access when neither', () => {
expect(canAccess(false, false)).toBe(false);
});
Pattern: Avoid Identity Values
it('calculates', () => {
expect(multiply(10, 1)).toBe(10);
expect(add(5, 0)).toBe(5);
});
it('calculates', () => {
expect(multiply(10, 3)).toBe(30);
expect(add(5, 3)).toBe(8);
});
Pattern: Verify Side Effects
it('processes order', () => {
processOrder(order);
});
it('processes order', () => {
processOrder(order);
expect(orderRepository.save).toHaveBeenCalledWith(order);
expect(emailService.send).toHaveBeenCalledWith(
expect.objectContaining({ to: order.customerEmail })
);
});
Summary: Mutation Testing Mindset
The key question for every line of code:
"If I introduced a bug here, would my tests catch it?"
For each test, verify it would catch:
- Arithmetic operator changes
- Boundary condition shifts
- Boolean logic inversions
- Removed statements
- Changed return values
Remember:
- Coverage measures execution, mutation testing measures detection
- A test that doesn't make assertions can't kill mutants
- Boundary values are critical for conditional mutations
- Avoid identity values that make operators interchangeable
Quick Reference
Operators Most Likely to Have Surviving Mutants
>= vs > (boundary not tested)
&& vs || (only tested when both true/false)
+ vs - (only tested with 0)
* vs / (only tested with 1)
some() vs every() (only tested with all matching)
Test Values That Kill Mutants
| Avoid | Use Instead |
|---|
| 0 (for +/-) | Non-zero values |
| 1 (for */) | Values > 1 |
| Empty arrays | Arrays with multiple items |
| Identical values for comparisons | Distinct values |
| All true/false for logical ops | Mixed true/false |