Stryker Mutation Testing for JavaScript: Setup, Config, and Reading Results
Stryker is the leading mutation testing framework for JavaScript and TypeScript. It supports Jest, Mocha, Jasmine, and Karma — and generates one of the clearest mutation reports available in any language ecosystem. Here's how to set it up and actually use the results.
Installation
Start by installing the Stryker CLI and the runner plugin for your test framework. For a Jest project:
npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runnerFor Mocha:
npm install --save-dev @stryker-mutator/core @stryker-mutator/mocha-runnerFor TypeScript projects, add the TypeScript checker:
npm install --save-dev @stryker-mutator/typescript-checkerConfiguration
Create stryker.conf.json in your project root. Here's a complete configuration for a typical Jest + TypeScript project:
{
"$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"testRunner": "jest",
"reporters": ["html", "clear-text", "progress"],
"coverageAnalysis": "perTest",
"mutate": [
"src/**/*.ts",
"!src/**/*.spec.ts",
"!src/**/*.test.ts",
"!src/**/index.ts"
],
"jest": {
"projectType": "custom",
"configFile": "jest.config.js",
"enableFindRelatedTests": true
},
"checkers": ["typescript"],
"tsconfigFile": "tsconfig.json",
"thresholds": {
"high": 80,
"low": 60,
"break": 50
},
"timeoutMS": 5000,
"concurrency": 4
}Key config options explained:
coverageAnalysis: "perTest"— Stryker maps which tests cover which code, then only runs relevant tests for each mutant. This dramatically speeds up runs. Use"off"only if you have dynamic test loading that breaks this.mutate— array of glob patterns for files to mutate. Always exclude test files and generated code.thresholds—highandlowset the green/yellow/red thresholds in the HTML report.breakfails the process with a non-zero exit code if the mutation score drops below it — useful for CI gates.concurrency— number of parallel test runner processes. Set to your CPU count minus one.timeoutMS— per-mutant timeout. Increase if your tests are slow; lower if mutants are hanging.
Running Stryker
Add a script to your package.json:
{
"scripts": {
"mutation": "stryker run"
}
}Then run:
npm run mutationStryker will:
- Run your full test suite to establish a baseline (if baseline fails, it stops)
- Instrument your source code to track which tests cover which lines
- Generate mutants for each file in
mutate - Run only the relevant tests for each mutant
- Report which mutants were killed vs survived
For large projects, the first run can take 20–60 minutes. Subsequent runs with coverageAnalysis: "perTest" are faster because Stryker skips mutants with no test coverage entirely.
Reading the HTML Report
After a run, open reports/mutation/mutation.html in your browser. The report has three views:
Overview dashboard — shows overall mutation score, total mutants, killed/survived/timeout/no-coverage counts. The score badge is color-coded: green (above high threshold), yellow (between low and high), red (below low).
File tree — each file shows its individual mutation score. Files with the worst scores are where to focus first.
Source view — click any file to see your source code annotated with mutant markers. Each mutant shows:
- The original code (highlighted)
- The mutated version
- Whether it was killed or survived
- Which test killed it (if killed)
Survived mutants appear in red. These are your action items.
Understanding Mutant Status
The HTML report shows five possible statuses:
| Status | Meaning |
|---|---|
| Killed | Tests caught this mutant — good |
| Survived | Tests passed with the bug — test gap |
| Timeout | Tests timed out — usually infinite loops introduced by mutant |
| No coverage | No test executes this code at all |
| Ignored | Excluded by --excludedMutations |
Survived is the primary concern. No coverage is worse — there's no test at all for that code path. Timeout mutants are usually fine to ignore; they indicate the mutant would cause infinite loops, which real tests would eventually catch.
Prioritizing Which Mutants to Kill
Not all survived mutants are equally important. Prioritize in this order:
1. Boundary condition mutants in business logic
// Original
if (orderTotal >= FREE_SHIPPING_THRESHOLD) {
applyFreeShipping(order);
}
// Mutant: >= becomes >
if (orderTotal > FREE_SHIPPING_THRESHOLD) {
applyFreeShipping(order);
}A boundary mutation like this means an order at exactly the threshold gets charged for shipping. High priority to kill.
2. Return value mutants in pure functions
// Original
function calculateTax(amount, rate) {
return amount * rate;
}
// Mutant: returns 0
function calculateTax(amount, rate) {
return 0;
}If your test only checks that calculateTax returns something, this survives. Add expect(calculateTax(100, 0.1)).toBe(10).
3. Boolean logic mutants
// Original
if (user.isActive && user.hasPermission('admin')) {
// ...
}
// Mutant: && becomes ||
if (user.isActive || user.hasPermission('admin')) {
// ...
}This is a real security bug. Kill it with a test that checks inactive users with admin permission are blocked.
4. String and array mutations in lower-risk code — address last.
Excluding Specific Mutators
Some mutators generate noise for your codebase. Disable them globally in config:
{
"mutator": {
"excludedMutations": [
"StringLiteral",
"ArrayDeclaration"
]
}
}Or ignore specific lines with a comment:
function getErrorMessage(code) {
// Stryker disable next-line StringLiteral
return `Error code: ${code}`;
}Use this sparingly. If you're ignoring a mutant because it's annoying rather than because it's genuinely equivalent, you're hiding a gap.
Incremental Mode
Stryker 6+ supports incremental mutation testing — only running mutations on code changed since the last run. Enable it:
{
"incremental": true,
"incrementalFile": ".stryker-tmp/incremental.json"
}Commit .stryker-tmp/incremental.json to your repo (or store it in CI cache). Incremental runs on PRs are typically 5–10x faster than full runs, making CI integration practical.
CI Integration
Add mutation testing to your pipeline with a threshold gate:
# GitHub Actions example
- name: Run mutation tests
run: npm run mutation
env:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}Set "break": 50 in your thresholds so the pipeline fails if mutation score drops below 50%. Gradually raise this threshold as your test suite improves.
The Stryker Dashboard (dashboard.stryker-mutator.io) stores historical mutation scores and generates a badge for your README. Connect it by setting the STRYKER_DASHBOARD_API_KEY environment variable and adding "dashboard" to your reporters.
Common First-Run Issues
Baseline tests fail — Stryker requires your tests to pass before it starts. Fix failing tests first.
Extremely slow runs — set coverageAnalysis: "perTest" and lower concurrency if you're memory-constrained.
All mutants show "No coverage" — your mutate glob doesn't match your source files, or coverageAnalysis can't instrument your code. Try coverageAnalysis: "off" to verify.
TypeScript type errors in mutants — expected. The TypeScript checker validates that type-safe mutants are generated, but some invalid mutations are filtered out before running. This is normal.
Mutation testing with Stryker takes an upfront investment to configure, but the HTML report makes it unusually clear where your test suite is weak. The surviving mutants are a prioritized list of exactly what to test next.