Mutation Testing in PHP: How to Write Tests That Actually Catch Bugs

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why 100% Code Coverage is an Illusion?

\n

Have 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.

\n

Imagine 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.

\n

In 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\n

What is Mutation Testing? (And Why It's Not Magic)

\n

Mutation 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.

\n

A 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.

\n

In PHP, the primary tool for this is Infection — a library that runs PHPUnit on each mutant and analyzes the results.

\n\n

What Does It Look Like in Practice?

\n

Let'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}
\n

And 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}
\n

The Infection tool will create a mutant by replacing + with -:

\n
return $a - $b;
\n

Then 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\n

Installing Infection and First Run

\n

First, install Infection via Composer:

\n
composer require --dev infection/infection
\n

Then create a configuration file infection.json (or run vendor/bin/infection for interactive setup).

\n

Example minimal configuration:

\n
{\n    "source": {\n        "directories": [\n            "src"\n        ]\n    },\n    "phpUnit": {\n        "configDir": "."\n    },\n    "mutators": {\n        "@default": true\n    }\n}
\n

Run mutation testing:

\n
vendor/bin/infection --min-msi=100
\n

Where --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.

\n\n

Types of Mutations in PHP (With Examples)

\n

Infection supports dozens of mutators. Here are the most common ones you'll encounter:

\n\n

1. Mathematical Operators

\n

Replacing + with -, * with /, % with *, etc.

\n
// Original code\nreturn $price * $quantity;\n\n// Mutant\nreturn $price / $quantity;
\n

Blogs

Book Recommendations