Verdict
Yes
Yes, mutation testing is worth it on the code each pull request changes: run StrykerJS or PIT on those files in CI and add an assertion for every surviving mutant that shows a real gap; do not run it over the whole codebase or set a score target.
Why
- Blast radiususers
- Change frequencyregularly
- Detectabilityeventually
- Reversibilitywith-effort
- Test costmoderate
Yes, mutation testing is worth it when you run it on the code you change. The typical case is a customer-facing web application with unit tests in CI; the mutation run is the test, and it prevents bugs that the existing tests let through. Blast radius is users and Change frequency is regularly, since each monthly change can leave a line that tests run but do not check. Detectability is eventually: a test that runs a line still fails on an exception, so the bugs that slip past it are wrong values that raise no error. Reversibility is with-effort, because records written in the meantime need a repair script, and Test cost is moderate, an hour of setup plus minutes per pull request, so rule R11 gives Test.
| When | Decision | Why |
|---|---|---|
| The changed code calculates charges, refunds or invoice totals | Test mandatory: close every surviving mutant with a test or a written reason | Blast radius rises to money and Reversibility to costly, because a wrong charge ends in refunds |
| The changed code decides who may read a record | Test mandatory: kill every mutant that flips a condition in the access check | Blast radius rises to safety-or-legal and Detectability to never, because a leaked record raises no error |
| The changed code is a parsing module that changes a few times a year | Test minimally: one mutation run on the module after each change | Change frequency falls to rarely, so a test gap has fewer changes in which to let a bug through |
| Only browser end-to-end tests cover the changed code, so each mutant costs a three-minute run | Test it differently: skip mutation runs on that code and alert on the values it writes in production | Test cost rises to heavy while Detectability stays eventually and Reversibility with-effort |
| The surviving mutants sit in button labels and CSS class names that the developer sees after each change | Do not test: exclude those files from the mutation run | Detectability moves to immediately and Reversibility to trivial, because a revert removes a wrong label |
| The code is a prototype that only you run | Do not test: skip mutation runs until someone else uses the code | Blast radius falls to none, because you alone bear a failure |
What breaks if you don't test
A test that asserts only the length of a result gives full line coverage and checks almost nothing. A later change flips a comparison, CI stays green, and customers get a wrong list. Nobody links the bug to the change for days, because no error fired and the coverage report still shows 100%.
What you lose if you over-test
A full run mutates almost every line and reruns the covering tests for each mutant, so on a large project it moves to a nightly job that nobody reads. Some survivors are equivalent mutants, such as i < n turned into i != n in a loop that counts up by one, and each costs minutes to judge. A score target makes people assert log messages to kill mutants no user would notice.
How to test
Run it at the unit level, in CI, on the files a pull request changes:
- Add StrykerJS for TypeScript or PIT for Java, and run it on one module to measure the time per mutant.
- In CI, limit the run to changed files (
--mutatein StrykerJS,targetClassesin PIT), or use StrykerJS incremental mode. - For each surviving mutant, add an assertion that kills it, or record why it is equivalent.
- Report survivors in the pull request instead of failing the build on a score, as Google does for more than 24,000 developers (Petrović et al., 2021).
When the answer changes
- The code moves money or decides who may see a record.
- Only slow end-to-end tests cover the code you change.
- The project has no unit tests yet, so every mutant survives and the tests come first.
Real incident + Code example
The festival that vanished on its last day
On a city events site I worked on, upcoming() kept events ending today or later, and its only test asserted the result's length for a date on which no event ended. A refactor to timestamps checked endsAt > now, with endsAt set to midnight at the start of the last day. Festivals dropped off the listing on their final day, the test stayed green, and an organiser emailed 19 days later. StrykerJS on the original e.endDate >= today leaves two mutants alive against that test, > and <, and the second test below kills both:
import { describe, expect, it } from "vitest";
import { upcoming } from "./events"; // events.filter((e) => e.endDate >= today)
const ev = (endDate: string) => ({ title: endDate, endDate });
describe("upcoming", () => {
// Full line coverage; the > and < mutants survive
it("returns upcoming events", () => {
const events = [ev("2026-01-01"), ev("2026-12-31")];
expect(upcoming(events, "2026-06-01")).toHaveLength(1);
});
// One event ends before, one on, one after the date
it("keeps an event on its last day and drops a finished one", () => {
const events = [ev("2026-05-31"), ev("2026-06-01"), ev("2026-06-02")];
expect(upcoming(events, "2026-06-01").map((e) => e.endDate)).toEqual([
"2026-06-01",
"2026-06-02",
]);
});
});
Related questions
FAQ
- Is mutation testing useful in practice?
Yes, mutation testing is useful in practice when it runs on changed code and reports surviving mutants in the pull request. Google runs mutation testing on changed code during code review, according to Petrović et al. (2021).
- What mutation score should I aim for?
Set no mutation score target; review each surviving mutant in the code you changed instead. For code that calculates money or checks access, close every surviving mutant with a test or a written reason.
- How long does mutation testing take?
A mutation run takes the number of mutants multiplied by the time of the tests that cover each one. On a pull request that touches a few files covered by unit tests, a run limited to those files takes minutes.
- Is mutation testing better than code coverage?
Mutation testing shows whether tests check results; line coverage shows only whether tests run the lines. A test without an assertion gives 100% line coverage and kills no mutant.