Stryker.js: JavaScript and TypeScript Mutation Testing
Stryker is the leading mutation testing framework for JavaScript and TypeScript. It supports Jest, Vitest, Mocha, Jasmine, and more — integrating into your existing test setup without requiring you to change your tests. If your tests pass but you don't know whether they'd catch a real bug, Stryker is how you find out.
How Stryker Works
Stryker applies mutations to your source code and reruns your test suite for each mutant:
- Runs your tests once (static analysis baseline)
- Generates mutants — small code changes (operators, conditions, return values)
- For each mutant: runs only the tests that cover that code path
- Reports which mutants survived (your tests missed the bug) vs. killed (tests caught it)
The per-test coverage analysis (coverageAnalysis: "perTest") makes Stryker much faster than naive approaches by running only relevant tests per mutant.
Installation
# Core package
npm install --save-dev @stryker-mutator/core
<span class="hljs-comment"># Test runner plugin (choose yours)
npm install --save-dev @stryker-mutator/jest-runner <span class="hljs-comment"># Jest
npm install --save-dev @stryker-mutator/vitest-runner <span class="hljs-comment"># Vitest
npm install --save-dev @stryker-mutator/mocha-runner <span class="hljs-comment"># MochaConfiguration
Create stryker.config.json:
{
"$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"testRunner": "jest",
"coverageAnalysis": "perTest",
"mutate": [
"src/**/*.ts",
"!src/**/*.test.ts",
"!src/**/*.spec.ts",
"!src/generated/**"
],
"reporters": ["html", "clear-text", "progress", "json"],
"htmlReporter": {
"fileName": "reports/mutation/index.html"
},
"thresholds": {
"high": 80,
"low": 60,
"break": 50
},
"timeoutMS": 10000,
"concurrency": 4
}Run:
npx stryker runVitest Configuration
{
"testRunner": "vitest",
"coverageAnalysis": "perTest",
"vitest": {
"configFile": "vitest.config.ts"
},
"mutate": ["src/**/*.ts", "!src/**/*.test.ts"]
}Understanding Mutations
Stryker applies these mutation types by default:
ArithmeticOperator: a + b → a - b, a * b → a / b
// Original
const total = price * quantity;
// Mutant (ArithmeticOperator)
const total = price / quantity;ConditionalExpression: flips boolean conditions
// Original
if (user.isActive && user.hasPermission) { ... }
// Mutant (ConditionalExpression)
if (false) { ... }EqualityOperator: changes comparison operators
// Original
if (count > 0) { ... }
// Mutant
if (count >= 0) { ... } // boundary mutation
if (count < 0) { ... } // full negationLogicalOperator: swaps && and ||
// Original
if (isAdmin || isModerator) { ... }
// Mutant
if (isAdmin && isModerator) { ... }StringLiteral: changes string values
// Original
return 'success';
// Mutant
return '';Reading the HTML Report
Open reports/mutation/index.html. The dashboard shows:
- Mutation score (top level and per file)
- Killed: mutants your tests caught
- Survived: mutants that need attention
- No coverage: code not reached by any test
- Timeout: tests that hung on a mutant (usually means infinite loop detection)
- Compile error: mutant caused a compile error (auto-killed, doesn't count)
Click into any file to see source annotations. Red lines have surviving mutants; expand them to see what changed:
Line 42: Survived
Original: if (items.length > 0)
Mutant: if (items.length >= 0)This tells you: your test never passes an empty array and asserts the right behavior.
A TypeScript Example
// src/pricing.ts
export function calculateDiscount(price: number, memberLevel: string): number {
if (price <= 0) throw new Error('Price must be positive');
if (memberLevel === 'gold') return price * 0.2;
if (memberLevel === 'silver') return price * 0.1;
return 0;
}Weak tests:
it('calculates gold discount', () => {
const discount = calculateDiscount(100, 'gold');
expect(discount).toBeTruthy(); // survives: 0.2 → 0.1 (still truthy)
});
it('returns zero for unknown level', () => {
const result = calculateDiscount(100, 'bronze');
expect(result).toBeDefined(); // survives: return 0 → return 1 (still defined)
});Surviving mutants Stryker finds:
* 0.2→/ 0.2— exact value not asserted* 0.1→/ 0.1— same issuereturn 0→return 1— return value not checkedprice <= 0→price < 0— boundary not tested (price = 0 not checked)
Strong tests that kill these:
it('calculates exact gold discount', () => {
expect(calculateDiscount(100, 'gold')).toBe(20); // exact value
});
it('calculates exact silver discount', () => {
expect(calculateDiscount(100, 'silver')).toBe(10);
});
it('returns zero for unknown member level', () => {
expect(calculateDiscount(100, 'bronze')).toBe(0); // exact zero
});
it('throws on zero price', () => {
expect(() => calculateDiscount(0, 'gold')).toThrow('Price must be positive');
});
it('throws on negative price', () => {
expect(() => calculateDiscount(-1, 'gold')).toThrow('Price must be positive');
});CI/CD Integration
GitHub Actions:
name: Mutation Testing
on:
push:
branches: [main]
pull_request:
jobs:
stryker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- run: npx stryker run
- uses: actions/upload-artifact@v3
if: always()
with:
name: stryker-report
path: reports/mutation/If mutation score falls below break (50%), npx stryker run exits with a non-zero code, failing the build.
Scoping Stryker to Critical Code
Stryker is slow on large codebases. Don't mutate everything — focus on business-critical logic:
{
"mutate": [
"src/billing/**/*.ts",
"src/auth/**/*.ts",
"src/validation/**/*.ts",
"!src/**/*.test.ts",
"!src/**/*.spec.ts"
]
}UI components, utility helpers, and configuration code are lower priority. Business logic, validation, and calculations are where mutation testing delivers the most value.
Incremental Mutation Testing
Stryker supports incremental mode to only test changed files:
npx stryker run --incrementalOn first run, creates a .stryker-tmp/incremental.json cache. Subsequent runs skip mutants that haven't changed, making CI runs much faster.
Common Configuration Mistakes
Missing coverageAnalysis: Without "perTest", Stryker runs all tests for every mutant — 10x slower. Always set it.
Too broad mutate patterns: Mutating node_modules or generated files wastes time and produces noise.
Threshold too low: Setting break: 0 effectively disables enforcement. Start at 50%, work up to 75%+.
No reporters: The default only shows a summary. Add "html" to get the visual report you actually need to act on results.
Pair with Continuous Functional Testing
Stryker improves your unit and integration test quality. For continuous monitoring of your JavaScript/TypeScript application in production — verifying user flows, API behavior, and UI interactions — HelpMeTest provides AI-powered test automation without requiring code.
High Stryker mutation score + HelpMeTest functional coverage = confidence your application is tested end to end.
Start free with HelpMeTest — 10 tests, no code required, monitoring every 5 minutes.