Introduction: Why 100% Code Coverage is an Illusion?
\nHave you ever been proud of a PHPUnit report that says "Code Coverage: 98%"? And then you deployed a release, and a critical bug crashed in production. Sound familiar? The problem is that classic coverage (line/statement coverage) only measures which lines of code were executed. It does not check whether your tests actually verify the logic.
\nImagine you wrote a test that checks if a function returns true. But the function itself always returns true, even if you break it. The test will pass, coverage will be 100%, but you won't catch the bug. This is exactly where mutation testing in PHP comes to the rescue — a technique that turns the conventional understanding of testing upside down.
\nIn this article, we'll break down what mutation testing is, how it works using real PHP code as an example, and how to integrate it into your project using the Infection library.
\n\nWhat is Mutation Testing? (And Why It's Not Magic)
\nMutation testing is a method for evaluating test quality where small, intentional changes (mutations) are introduced into the source code. If the test fails after the change — the mutant is killed, the test works. If the test remains green — the mutant survived, meaning your test doesn't actually check anything.
\nA simple analogy: you are a security guard, and your tests are cameras. Mutation testing sends "intruders" (mutants) into the building. If the camera (test) spots the intruder — it works. If the intruder walks around freely — the camera is useless.
\nIn PHP, the primary tool for this is Infection — a library that runs PHPUnit on each mutant and analyzes the results.
\n\nWhat Does It Look Like in Practice?
\nLet's say you have a simple calculator class:
\n<?php\nclass Calculator\n{\n public function add(int $a, int $b): int\n {\n return $a + $b;\n }\n}\nAnd a test for it:
\n<?php\nuse PHPUnit\\Framework\\TestCase;\n\nclass CalculatorTest extends TestCase\n{\n public function testAdd()\n {\n $calc = new Calculator();\n $result = $calc->add(2, 3);\n $this->assertEquals(5, $result);\n }\n}\nThe Infection tool will create a mutant by replacing + with -:
return $a - $b;\nThen it will run the test. If the test fails — the mutant is killed. If the test passes (and it will, because you only check 2+3, not 2-3) — the mutant survived. This is a signal: your test is not good enough.
\n\nInstalling Infection and First Run
\nFirst, install Infection via Composer:
\ncomposer require --dev infection/infection\nThen create a configuration file infection.json (or run vendor/bin/infection for interactive setup).
Example minimal configuration:
\n{\n "source": {\n "directories": [\n "src"\n ]\n },\n "phpUnit": {\n "configDir": "."\n },\n "mutators": {\n "@default": true\n }\n}\nRun mutation testing:
\nvendor/bin/infection --min-msi=100\nWhere --min-msi=100 means we want 100% of mutants to be killed (an ideal, but rarely achievable metric). Infection will create an HTML report (folder infection.html), where surviving mutants will be marked in red.
Types of Mutations in PHP (With Examples)
\nInfection supports dozens of mutators. Here are the most common ones you'll encounter:
\n\n1. Mathematical Operators
\nReplacing + with -, * with /, % with *, etc.
// Original code\nreturn $price * $quantity;\n\n// Mutant\nreturn $price / $quantity;\n
Blogs
Book Recommendations
Read also
SQLite in Python: Practical Tips for Working with Databases
TypeScript Community: From Beginner to Pro in One Evening
Clean Code in C#: Principles of Writing Readable Code with Examples
Time is Money: How to Put a C++ Thread to Sleep for Milliseconds
BDD in JavaScript: Writing Tests That Business Understands