Are you sure your tests actually work?
\n\nImagine: you have 95% code coverage, all tests are green, CI/CD passes without errors. The team is happy, the manager is calm. But one day a bug surfaces in production. You open the code, look at the tests, and realize: they simply check that the function returns a number, but don't look at its value. Or worse — the test is written in such a way that it passes no matter what.
\n\nSound familiar? Welcome to the world where coverage percentage is just a number, not a quality guarantee. This is where mutation testing enters the stage — a methodology that truly makes your tests suffer and prove their worth.
\n\nIn this article, we'll break down how to implement mutation testing in a TypeScript project using Stryker, and why it could become your new favorite tool (or a new source of headaches).
\n\nWhat is mutation testing in simple terms?
\n\nMutation testing is when you take your code, intentionally introduce small errors (mutations) into it, and see if your tests fail. If the tests don't fail — it means you're testing incorrectly. If they fail — well done, the mutant is killed.
\n\nSounds like sadism? Possibly. But it works. Imagine you're a QA engineer who secretly snuck into the code and replaced === with ==, > with <, or removed a function call. A good test must catch this. A bad one will let it through.
In TypeScript, with its strict typing, mutation testing is especially useful: it catches cases where the types are correct, but the logic is not.
\n\nWhy is regular code coverage an illusion?
\n\nLet's say you have a function:
\n\nfunction calculateDiscount(price: number, isPremium: boolean): number {\n if (isPremium) {\n return price * 0.8;\n }\n return price;\n}\n\nAnd a test:
\n\ntest('returns number', () => {\n const result = calculateDiscount(100, true);\n expect(typeof result).toBe('number');\n});\n\nCoverage? 100%. Test? Passes. But what if we replace price * 0.8 with price * 0.5? The test will still pass — it only checks the type, not the value. There you have it — a false sense of security.
Mutation testing will highlight such a mutation and say: "Hey, your test didn't notice I changed the discount!". And you'll have to write a proper check.
\n\nHow mutation testing works in TypeScript: step by step
\n\nThe entire process can be broken down into three stages:
\n\n- \n
- Mutant generation — the tool copies your code and makes one change at a time. For example, it changes
&&to||or removes a method call. \n - Running tests — your tests are run for each mutant. \n
- Result analysis — if the tests fail, the mutant is "killed". If not, it "survived", and that's a risk zone. \n
The main metric is the mutation score (percentage of killed mutants). 100% is ideal, but in practice 80-90% is already excellent.
\n\nTool #1 for TypeScript: Stryker
\n\nIn the world of TypeScript and JavaScript, the king of mutation testing is Stryker (StrykerJS). It's fast, works with Jest, Vitest, Mocha, and even browser tests. It can be configured in 5 minutes.
\n\nLet's install it:
\n\nnpm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner\n\nCreate a config file stryker.conf.js:
// stryker.conf.js\nexport default {\n mutate: ['src/**/*.ts', '!src/**/*.spec.ts'], // what to mutate\n testRunner: 'jest',\n reporters: ['html', 'clear-text', 'progress'],\n jest: {\n projectType: 'custom',\n configFile: 'jest.config.ts',\n },\n concurrency: 4, // how many mutants