Mutation Testing — Cheat Sheet

Tags: mutation-testing, stryker, testing, qa, code-quality, cicd

Level: intermediate

TL;DR: Mutation testing validates test suite effectiveness by injecting artificial faults (mutants) and measuring the percentage of mutants your tests catch and kill.

Key concepts

Evaluates test quality beyond simple code coverage by checking assertion effectiveness.

Introduces small syntactic changes (mutations) into source code to create "mutants".

A mutant is considered "killed" when at least one automated test fails.

A mutant "survives" when all tests pass despite the introduced fault.

Mutation score is calculated as killed mutants divided by total non-equivalent mutants.

Mutation operators modify code patterns: boundary conditions, arithmetic symbols, logical negations, return values.

Equivalent mutants produce identical program behavior to original code and cannot be killed.

High computational cost due to executing the test suite against every generated mutant.

Test coverage shows executed lines; mutation score reveals if tests actually assert behavior.

Typical questions and ready answers

Q: Why do we need mutation testing if we already have 90% code coverage?R: Coverage measures lines executed, not assertions verified. Mutation testing alters code logic; if tests still pass, it proves our assertions are weak or missing.

Q: How do you handle equivalent mutants?R: I exclude them via tooling configurations, ignore patterns, or refactor the code to eliminate dead and redundant logic paths.

Q: How do you keep mutation testing fast in CI/CD?R: I run it incrementally on git diffs, target only critical business logic, and use parallel test runners with mutant bailouts.

Pitfalls and common mistakes

Running full mutation runs on entire codebases in every single pull request build.

Treating 100% mutation score as a hard requirement instead of prioritizing critical domains.

Misinterpreting slow tests or infinite loops generated by mutants without setting strict timeouts.

Manually hunting equivalent mutants instead of refactoring obscure logic or suppressing non-actionable lines.

Snippets and commands

# Run Stryker for JavaScript/TypeScript projects
npx stryker run

# Run Stryker only on changed files against main branch
npx stryker run --since main

JSON

// stryker.config.json
{
  "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
  "testRunner": "jest",
  "mutator": {
    "excludedMutations": ["StringLiteral"]
  },
  "mutate": [
    "src/core/**/*.ts",
    "!src/**/*.spec.ts"
  ],
  "thresholds": { "high": 80, "low": 60, "break": 50 }
}

TypeScript

// Original function
function isAdult(age: number): boolean {
  return age >= 18;
}

// Mutants generated by operators
function isAdult_Mutant1(age: number): boolean {
  return age > 18; // Boundary condition flipped
}

function isAdult_Mutant2(age: number): boolean {
  return true; // Return value substituted
}

To review

[ ] Stryker configuration options for incremental runs and bail modes

[ ] Identifying equivalent mutants versus missing edge-case assertions

[ ] Integrating mutation score gates into PR checks without degrading pipeline speed

Jarvis Interviews
Jarvis