Stryker Mutator: Mutation Testing for JavaScript and TypeScript

Stryker Mutator: Mutation Testing for JavaScript and TypeScript

Stryker Mutator is the leading mutation testing framework for JavaScript and TypeScript. It supports Jest, Vitest, Jasmine, Mocha, and Karma test runners, and works with React, Angular, Node.js, and any other JS/TS project.

Installation

# Install Stryker CLI
npm install -D @stryker-mutator/core

# Add Jest runner
npm install -D @stryker-mutator/jest-runner

# Add Vitest runner
npm install -D @stryker-mutator/vitest-runner

# Add TypeScript support
npm install -D @stryker-mutator/typescript-checker

Or use the init command to auto-configure:

npx stryker init

This wizard detects your test runner and creates stryker.config.mjs.

Configuration

// stryker.config.mjs
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
export default {
  packageManager: 'npm',
  reporters: ['html', 'clear-text', 'progress', 'json'],
  testRunner: 'jest',
  coverageAnalysis: 'perTest',
  
  mutate: [
    'src/**/*.ts',
    '!src/**/*.test.ts',
    '!src/**/*.spec.ts',
    '!src/**/index.ts',
  ],
  
  thresholds: {
    high: 80,
    low: 60,
    break: 50,  // Fail build if score < 50%
  },
  
  jest: {
    projectType: 'custom',
    configFile: 'jest.config.ts',
  },
};

Running Stryker

# Run mutation tests
npx stryker run

# Run on specific files
npx stryker run --mutate src/services/order.ts

# Use a different config file
npx stryker run --configFile stryker.config.ci.mjs

# Dry run (list mutations without running tests)
npx stryker run --dryRunTimeoutMinutes 5

The HTML report is written to reports/mutation/mutation.html.

Vitest Runner

npm install -D @stryker-mutator/vitest-runner
// stryker.config.mjs
export default {
  testRunner: 'vitest',
  coverageAnalysis: 'perTest',
  vitest: {
    configFile: 'vitest.config.ts',
  },
  mutate: ['src/**/*.ts', '!src/**/*.test.ts'],
};

TypeScript Checker

Enable the TypeScript checker to reject mutations that don't compile — filtering out false positives:

npm install -D @stryker-mutator/typescript-checker
// stryker.config.mjs
export default {
  checkers: ['typescript'],
  tsconfigFile: 'tsconfig.json',
  testRunner: 'vitest',
  mutate: ['src/**/*.ts', '!src/**/*.test.ts'],
};

Mutations that produce TypeScript errors are filtered before running tests, reducing total mutations and improving performance.

Coverage Analysis

coverageAnalysis controls how Stryker determines which tests cover each mutant:

coverageAnalysis: 'perTest',  // Most accurate — tracks per-test coverage
coverageAnalysis: 'all',      // Run all tests for each mutant (slower, more accurate for edge cases)
coverageAnalysis: 'off',      // No coverage analysis — run all tests for every mutant

perTest is the best balance. With perTest, Stryker only runs tests that actually cover the mutated code — reducing test runs by 80–95% compared to off.

Mutation Operators

Stryker applies dozens of operators by default. You can configure specific ones:

mutatorDescription: {
  excludedMutations: [
    'StringLiteral',     // Skip string literal mutations
    'ObjectLiteral',     // Skip object literal mutations
    'ArrayDeclaration',  // Skip array literal mutations
  ],
},

Or use the --excludedMutations flag:

npx stryker run --excludedMutations StringLiteral

Common mutations Stryker applies:

  • ArithmeticOperator: + → -, * → /, % → *
  • BlockStatement: Remove entire block bodies
  • BooleanLiteral: true → false
  • ConditionalExpression: a ? b : c → b, c
  • EqualityOperator: === → !==, < → >=
  • LogicalOperator: && → ||
  • StringLiteral: Replace string with empty string
  • UnaryOperator: -x → x, !x → x

Reading the Report

The HTML report shows each file with color-coded mutations:

  • Green (Killed): Your tests caught this mutation
  • Red (Survived): Your tests missed this bug
  • Yellow (No coverage): No test covers this code at all
  • Gray (Timeout): Mutation caused an infinite loop

Click a survived mutation to see:

  • The original code
  • The mutated code
  • Which tests ran against it

Focus on red mutations in business-critical code paths.

Example: React Component

// src/components/PriceTag.tsx
export function PriceTag({ price, discount }: { price: number; discount?: number }) {
  const finalPrice = discount ? price * (1 - discount) : price;
  return <span>${finalPrice.toFixed(2)}</span>;
}
// src/components/PriceTag.test.tsx
test('shows original price without discount', () => {
  render(<PriceTag price={100} />);
  expect(screen.getByText('$100.00')).toBeInTheDocument();
});

test('shows discounted price', () => {
  render(<PriceTag price={100} discount={0.1} />);
  expect(screen.getByText('$90.00')).toBeInTheDocument();
});

Stryker would mutate 1 - discount to 1 + discount. With price=100 and discount=0.1, the mutated result is $110.00. Your test for $90.00 catches it — mutation killed.

Stryker might also mutate discount ? price * (1 - discount) : price by removing the ternary and just returning price. If your only discount test happens to work with a discount of 0 (which it doesn't here), that mutation could survive.

Scoping Mutations

mutate: [
  // Include business logic
  'src/services/**/*.ts',
  'src/domain/**/*.ts',
  'src/utils/**/*.ts',
  
  // Exclude these
  '!src/**/*.test.ts',
  '!src/**/*.spec.ts',
  '!src/**/*.stories.ts',
  '!src/**/index.ts',
  '!src/types/**/*.ts',
  '!src/generated/**/*.ts',
],

Performance

Stryker with coverageAnalysis: 'perTest' is reasonably fast. For a project with 200 tests and 50 mutations, expect 3–10 minutes depending on test speed.

Speed improvements:

  • Use coverageAnalysis: 'perTest' (default is all in older versions)
  • Exclude slow integration tests from the test runner config
  • Increase concurrency: concurrency: 4
  • Only mutate changed files in CI (use --since with Stryker Dashboard)
// stryker.config.mjs
export default {
  concurrency: 4,
  coverageAnalysis: 'perTest',
  disableTypeChecks: true,  // Skip type checking for speed
};

CI Integration

# .github/workflows/mutation-tests.yml
name: Mutation Tests
on:
  schedule:
    - cron: '0 3 * * 1'  # Weekly on Monday at 3 AM
  workflow_dispatch:  # Allow manual trigger

jobs:
  stryker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - name: Run Stryker
        run: npx stryker run
      - name: Upload mutation report
        uses: actions/upload-artifact@v4
        with:
          name: mutation-report
          path: reports/mutation/

Stryker Dashboard

Stryker offers a free hosted dashboard that tracks mutation scores over time:

// stryker.config.mjs
export default {
  dashboard: {
    project: 'github.com/my-org/my-repo',
    version: process.env.GITHUB_SHA,
  },
  reporters: ['html', 'json', 'dashboard'],
};
# Set your dashboard API key
STRYKER_DASHBOARD_API_KEY=${{ secrets.STRYKER_DASHBOARD_API_KEY }} npx stryker run

The dashboard shows your mutation score trend over time and can post PR comments with mutation results.

Summary

Stryker for JavaScript/TypeScript:

  1. npm install -D @stryker-mutator/core @stryker-mutator/jest-runner (or vitest-runner)
  2. Configure mutate to target business logic, exclude tests and generated code
  3. Use coverageAnalysis: 'perTest' for performance
  4. Add TypeScript checker to filter non-compiling mutations
  5. Run on a schedule (weekly/nightly) — not on every commit
  6. Fix surviving mutations by adding targeted assertions

The mutation score is a lagging indicator of test quality. A score rising over weeks means your test culture is improving.

Read more

Start now free