Code coverage tells you which lines your tests execute, which is not the same as which lines your tests actually check. A test can run a line and assert nothing meaningful about it. Infection exposes that gap with mutation testing. It makes small changes to your code - flips a comparison, changes a boolean, alters a return - and reruns your tests. If your tests still pass with the code deliberately broken, they weren't really testing that behavior, and Infection reports the surviving mutation.
Coverage That Actually Means Something
You run it against code that already has tests, and it grades how well those tests hold:
# Run mutation testing on your source
vendor/bin/infection
# It reports a Mutation Score Indicator (MSI):
# mutations killed = tests caught the change
# mutations survived = a gap in your tests
Find the Weak Assertions
The list of surviving mutations is a precise to-do list of where your tests are weak - places where you could break the code and nobody would notice. That's far more actionable than a coverage percentage, because it points at behavior you think is tested but isn't.
Rough Edges and Limitations
- Slow by nature. Running the suite once per mutation is expensive; scope it to changed code or run it in CI.
- Needs decent tests first. It grades existing tests - it isn't a substitute for writing them.
- Some noise. A few mutations are equivalent or irrelevant and need filtering.
When you want to know whether your test suite is actually protecting you, mutation testing is the honest answer - and Infection is the PHP tool for it.
Internal link suggestion: link "code that already has tests" to the PHPUnit or Pest post, since Infection grades the tests they write.