The mutation test that proved nothing
When I write a test after the code — which happens, whatever the ideal says — I do not trust it until I have watched it fail. A test written against working code passes immediately, and a passing test that has never failed has demonstrated nothing. It might be asserting something trivially true. It might be pointed at the wrong thing entirely.
So I break the code deliberately and check the test catches it. Twice now, that check itself has been worthless, in two different ways.
The first failure: the mutation broke compilation
I had written a test asserting that a legally required disclosure always renders next to a generated affiliate link. Displaying it is a condition of the program — omit it and you can lose the account and the earnings — so I wanted the test to be real, not decorative.
The test passed. To verify it meant something, I deleted the block of markup that renders the disclosure and re-ran.
Test Files 1 failed
Tests no tests
"No tests." Not a failing assertion — nothing ran at all. My crude deletion had left invalid markup, the file no longer compiled, and the runner could not load it.
That is a red result, and if I had been moving quickly I might have read "red, good, the test catches it" and moved on. But it proves nothing about the test. It proves the file was broken. A test that never executed cannot demonstrate that it detects anything.
The fix: mutate a value, not a structure
The second attempt changed the disclosure constant to an empty string:
- disclosure: "As an Amazon Associate I earn from qualifying purchases.",
+ disclosure: "",
That compiles cleanly. And the result was what I actually wanted to see:
× carries the exact disclosure the program requires
× shows the disclosure whenever it shows a link
Tests 2 failed | 20 passed
Two failures, both specific, both naming the property I care about — and eighteen other tests still passing, which tells me the mutation was targeted rather than a blast radius.
The general rule: mutate values, not syntax. Changing a constant, flipping a comparison, swapping a return value — these keep the program valid and isolate exactly one behaviour. Deleting a block tests your build.
The second failure: the mutation landed somewhere else
The earlier version of this mistake was subtler and I like it more, because nothing about the output looked wrong.
I wanted to prove a test would catch a dangerous reordering — a delete happening before validation instead of after. I applied the mutation by searching for an anchor string and patching around it. The suite ran. Everything passed.
I nearly concluded the test was worthless. In fact the anchor string appeared many times across a seven-thousand-line file, and my patch had landed in a different function than the one under test. The mutation was real, it compiled, it ran — and it was in the wrong place. The test was fine. My verification was not.
A mutation test has three ways to lie: the mutation did not compile, the mutation did not land where you meant, or the mutation did not actually change behaviour. All three produce a result you can misread as an answer.
The checklist I use now
- Confirm the mutation landed. Diff the file. If you patched by string match, verify the match was unique — that one was expensive to learn.
- Confirm the code still compiles and the suite still runs. "No tests" and "0 passed" are not failures in the sense you need.
- Confirm the right tests failed, by name. Not "some things went red." The specific assertions covering the specific property.
- Confirm unrelated tests still pass. If everything goes red, you broke something structural and learned nothing about the one behaviour.
- Revert, and re-run to green before you commit anything.
Why bother
This is maybe two minutes of work per test that matters, and I do not do it for every test. I do it when a test is the only thing standing between me and a consequence I cannot undo — a legal disclosure vanishing from a page, a delete running against unvalidated data.
For those, the question is not "do I have coverage." It is "have I seen this test catch the thing it exists to catch." Those are very different claims, and only one of them survives contact with a real failure.