Are Your Tests Lying? Time for Mutation Testing
\n\nYou 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\nThis 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.
What is Mutation Testing in Simple Terms
\n\nImagine 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\nIf 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.
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
Why Code Coverage is a False Sense of Security
\n\nMany 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\nAnd the "perfect" test:
\n\ntest('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\nCoverage: 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".
Tool #1: Stryker Mutator for JavaScript
\n\nThe 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\nInstallation and Quick Start
\n\nInstall Stryker globally or locally in your project:
\n\nnpm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner\n\nCreate a configuration file stryker.conf.json:
{\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\nRun the mutatio