Stryker Mutation Testing for JavaScript: Setup, Config, and Reading Results

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-runner

For Mocha:

npm install --save-dev @stryker-mutator/core @stryker-mutator/mocha-runner

For TypeScript projects, add the TypeScript checker:

npm install --save-dev @stryker-mutator/typescript-checker

Configuration

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.
  • thresholdshigh and low set the green/yellow/red thresholds in the HTML report. break fails 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 mutation

Stryker will:

  1. Run your full test suite to establish a baseline (if baseline fails, it stops)
  2. Instrument your source code to track which tests cover which lines
  3. Generate mutants for each file in mutate
  4. Run only the relevant tests for each mutant
  5. 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.

Read more