Mutation Testing in JavaScript: How to Catch Elusive Bugs

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Are Your Tests Lying? Time for Mutation Testing

\n\n

You write unit tests, the code coverage looks great — 85%, 90%, but bugs still pop up in production? A familiar pain. The classic coverage metric (line/branch coverage) is essentially measuring which lines of code were executed. But it says nothing about how well the tests verify the logic. They might just "run through" the lines without checking the correctness of the result.

\n\n

This is where mutation testing comes into play. It's like a vaccine shot for your code: we intentionally introduce small "viruses" (mutations) into the program and see if our tests kill them. If a test doesn't fail after changing === to !== — your tests are weak. In this article on pythonlib.ru, we'll break down how to implement mutation testing in a JavaScript project to make your code truly reliable.

\n\n

What is Mutation Testing in Simple Terms

\n\n

Imagine you're a QA engineer who secretly changes a line in the developer's code:

\n\n
// Before\nif (user.age >= 18) { allowAccess(); }\n\n// After (mutation)\nif (user.age > 18) { allowAccess(); }
\n\n

If after this replacement the unit tests are still green — the tests don't check the boundary condition 18. A mutation framework makes thousands of such replacements automatically and produces a report: how many mutations survived. The fewer surviving mutants — the more reliable your tests.

\n\n

Main types of mutations in JavaScript:

\n
    \n
  • Changing comparison operators: > → >=, === → !==
  • \n
  • Removing function calls: validate() → empty string
  • \n
  • Changing numeric literals: 0 → 1, 100 → 0
  • \n
  • Inverting boolean conditions: if (isActive) → if (!isActive)
  • \n
  • Substituting string values (less common, but happens)
  • \n
\n\n

Why Code Coverage is a False Sense of Security

\n\n

Many JavaScript developers are deluded by 100% code coverage. Let's look at a real example:

\n\n
// Function that determines a discount\nfunction getDiscount(price, isPremium) {\n    if (price > 100 && isPremium) {\n        return price * 0.8;\n    }\n    return price;\n}
\n\n

And the "perfect" test:

\n\n
test('should return price for non-premium', () => {\n    expect(getDiscount(50, false)).toBe(50);\n});\n\ntest('should return price for premium with low price', () => {\n    expect(getDiscount(50, true)).toBe(50);\n});
\n\n

Coverage: 100% lines, 100% branches. But the mutation price > 100 → price >= 100 will survive! The tests don't check the boundary value 100.01 or exactly 100. Mutation testing will instantly highlight this "blind spot".

\n\n

Tool #1: Stryker Mutator for JavaScript

\n\n

The most popular and mature tool for mutation testing in the JS world is Stryker Mutator (or simply Stryker). It supports Jest, Mocha, Jasmine, Karma and works with TypeScript.

\n\n

Installation and Quick Start

\n\n

Install Stryker globally or locally in your project:

\n\n
npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner
\n\n

Create a configuration file stryker.conf.json:

\n\n
{\n  "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",\n  "packageManager": "npm",\n  "reporters": ["html", "clear-text", "progress"],\n  "testRunner": "jest",\n  "coverageAnalysis": "perTest",\n  "thresholds": {\n    "high": 80,\n    "low": 60,\n    "break": 50\n  }\n}
\n\n

Run the mutatio

Blogs

Book Recommendations