9 unchanged lines
You may not read a test and then declare it worthless. That judgment certifies
You may not read a test and then declare it worthless. That judgment certifies
itself, and it always says yes. Run the mutation check instead.
itself, and it always says yes. Run the mutation check instead.
**If the repository already has a mutation runner, use it.** Look for Stryker,
**If the repository already has a mutation runner, use it.** Look for Stryker,
mutmut, go-mutesting, PIT, or a `test:mutation` script. Its configuration holds
mutmut, go-mutesting, PIT, or a `test:mutation` script. Its configuration holds
knowledge this skill does not have, such as which files repay the run and which
knowledge this skill does not have, such as which files repay the run and which
generate thousands of worthless mutants.
generate thousands of worthless mutants.
**The mutation scope must cover the subject file of every test you delete in the
**The mutation scope must cover the subject file of every test you delete in the
19 unchanged lines
unchanged total:
unchanged total:
1. **A trade.** Two mutants swap status and the total holds.
1. **A trade.** Two mutants swap status and the total holds.
2. **A disappearance.** You removed the source a mutant lived in, so the mutant
2. **A disappearance.** You removed the source a mutant lived in, so the mutant
is gone. Nothing moved from killed to survived, and the behaviour is now
is gone. Nothing moved from killed to survived, and the behaviour is now
untested. This is the one that catches seam removal.
untested. This is the one that catches seam removal.
3. **A scope change.** The second run covered fewer files than the first.
3. **A scope change.** The second run covered fewer files than the first.
So: record every baseline mutant identifier before you start, with the source
So: record every baseline mutant identifier before you start, with the source
hash and the exact test command. At the end, **every baseline mutant must still
hash and the exact test command. At the end, **every baseline mutant must still
exist and must still be killed.** A missing mutant fails the sweep exactly as a
exist and must still be killed.** A missing mutant fails the sweep exactly as a
survived one does.
survived one does.
The suite stays green throughout, so green proves nothing here.
The suite stays green throughout, so green proves nothing here.
The score is not proof either. It shows that the mutants this runner generated,
The score is not proof either. It shows that the mutants this runner generated,
under this operator set, over this file set, kept their status. Behaviour no
under this operator set, over this file set, kept their status. Behaviour no
operator reaches is invisible to it, and a dynamic caller found only through a
operator reaches is invisible to it, and a dynamic caller found only through a
configuration string or a plugin registry is invisible to your grep. **A sweep
configuration string or a plugin registry is invisible to your grep. **A sweep
62 unchanged lines
Time one scoped run before you plan the sweep. The cost ranges from seconds to
Time one scoped run before you plan the sweep. The cost ranges from seconds to
many minutes, so whether one run for each candidate is affordable depends on the
many minutes, so whether one run for each candidate is affordable depends on the
repository. When it is not, delete a batch and run once.
repository. When it is not, delete a batch and run once.
- The score holds. The whole batch stands.
- The score holds. The whole batch stands.
- The score falls. One test in the batch was the only killer of a mutant.
- The score falls. One test in the batch was the only killer of a mutant.
Restore the batch, halve it, and run again. Repeat until you find the test
Restore the batch, halve it, and run again. Repeat until you find the test
that matters, then keep it and delete the rest.
that matters, then keep it and delete the rest.
A batch proof is weaker than a proof for each test. It shows that no deletion
A batch proof is weaker than a proof for each test. It shows that no deletion
was the unique killer of a mutant. That is enough, and it is affordable.
was the unique killer of a mutant. That is enough, and it is affordable.
- Tests that assert that a call happened, when the call is not the contract
- Tests that assert that a call happened, when the call is not the contract
- Tests whose assertions restate the source line above them
- Tests whose assertions restate the source line above them
- Snapshot tests that you regenerate instead of read
- Snapshot tests that you regenerate instead of read
- Tests that exercise only a mock
- Tests that exercise only a mock
- Repeated coverage of one behavior across many cases
- Repeated coverage of one behavior across many cases
- Tests for a private function that a public function already reaches
- Tests for a private function that a public function already reaches
- The only test that covers a behavior, however ugly it is
- The only test that covers a behavior, however ugly it is
- Call assertions on a unit whose whole job is to drive other units. There the
- Call assertions on a unit whose whole job is to drive other units. There the
call and its arguments are the result, and the mutation score will show it
call and its arguments are the result, and the mutation score will show it
- Regression tests that name a defect or an issue
- Regression tests that name a defect or an issue
- Contract tests at a boundary that you do not own
- Contract tests at a boundary that you do not own
- Property tests and fuzz tests
- Property tests and fuzz tests
## Remove the seams
## Remove the seams
A deleted test may have been the only caller of a seam. Grep for each one.
A deleted test may have been the only caller of a seam. Grep for each one.
- An interface with one implementation
- An interface with one implementation
- A constructor parameter that only a test supplies
- A constructor parameter that only a test supplies
- An exported symbol that only tests import
- An exported symbol that only tests import
- A mock or fake, and the hook it plugs into
- A mock or fake, and the hook it plugs into
- A flag that only a test reads
- A flag that only a test reads
Delete the seam, or inline it. Follow each simplification to its end. When you
Delete the seam, or inline it. Follow each simplification to its end. When you
remove a seam and a wrapper becomes a pass-through, remove the wrapper too.
remove a seam and a wrapper becomes a pass-through, remove the wrapper too.
**A seam can have one caller, and that caller can be a test in another file.
**A seam can have one caller, and that caller can be a test in another file.
Leave that seam alone.** Report it as a candidate for a later sweep. A sweep that
Leave that seam alone.** Report it as a candidate for a later sweep. A sweep that
reaches past its own scope breaks files that nobody asked you to touch.
reaches past its own scope breaks files that nobody asked you to touch.
**A grep does not find every caller.** Dependency injection tokens, plugin
**A grep does not find every caller.** Dependency injection tokens, plugin
registries, configuration strings and generated registrations all reference a
registries, configuration strings and generated registrations all reference a
symbol without naming it in a way `rg` matches. Removing such a seam keeps every
symbol without naming it in a way `rg` matches. Removing such a seam keeps every
unit test green, removes the mutants that lived in it, and fails in production.
unit test green, removes the mutants that lived in it, and fails in production.
When a symbol is exported across a package boundary, treat the grep as
When a symbol is exported across a package boundary, treat the grep as
inconclusive and leave it.
inconclusive and leave it.
Close with the two scores and one table.
Close with the two scores and one table.
Tests: 26 -> 9 (-65%) Mutation score: 16/18 -> 16/18 (no change)
Tests: 26 -> 9 (-65%) Mutation score: 16/18 -> 16/18 (no change)