Are Your Tests an Illusion of Security?
Imagine: you have 95% code coverage, all tests are green, CI/CD passes. You are confident that your PHP code is protected from regressions. But is that really the case?
I myself lived in this illusion for a long time, until I encountered a situation: a test passed, even though I accidentally replaced && with || in a critical condition. The test simply didn't check that branch. The coverage was 100%, but the logic wasn't.
Meet: mutation testing — a technique that ruthlessly "kills" your tests to find out how reliable they actually are. In this article, we'll break down how to apply it in PHP using the powerful tool Infection, and why it changes the game.
What is Mutation Testing and Why Do You Need It?
Mutation testing is an approach where small, intentional changes (mutations) are introduced into your source code. For example, the operator + is changed to -, true to false, or an entire line is deleted. After each mutation, your tests are run.
If the test fails — the mutation is "killed," meaning the test actually checks that piece of code. If the test passes — the mutation "survived." This is a warning sign: either the test doesn't check that condition, or it is written incorrectly.
Unlike the "code coverage" metric, which only shows which lines were executed, mutation testing checks the quality of the checks. A high coverage percentage is not a guarantee, but the Mutation Score Indicator (MSI) is a real measure of reliability.
Why is This Important for a PHP Developer?
- Detecting "dead" tests — those that are always green but don't check anything.
- Identifying gaps in logic — for example, missed boundary conditions.
- Increasing confidence during refactoring — you know exactly which tests protect the code.
- Saving time — instead of manually auditing tests, you get an automated report.
Tools for Mutation Testing in PHP
The leader in the PHP arena is Infection (formerly known as Humbug). It integrates easily with PHPUnit and Pest, supports many mutations, and provides clear reports.
Other options: Humbug (the predecessor, but less active) and Mutagen (experimental). We will focus on Infection — it's the modern standard.
Practice: Setting Up Infection in a Project
Let's install Infection and run it on a real example. Suppose we have a simple class for calculating a discount:
<?php// src/DiscountCalculator.phpclass DiscountCalculator{ public function calculate(float $price, bool $isPremium): float { if ($isPremium) { return $price * 0.9; // 10% discount }
return $price; }}And a test for it:
<?php// tests/DiscountCalculatorTest.phpuse PHPUnit\\Framework\\TestCase;
class DiscountCalculatorTest extends TestCase{ public function testPremiumDiscount(): void { $calculator = new DiscountCalculator(); $result = $calculator->calculate(100.0, true); $this->assertEquals(90.0, $result); }
public function testRegularPrice(): void { $calculator = new DiscountCalculator(); $result = $calculator->calculate(100.0, false); $this->assertEquals(100.0, $result); }}Coverage is 100%. But let's check with mutations.
Installing and Running Infection
composer require --dev infection/infection./vendor/bin/infectionAfter