Property-based Testing in PHP: Breaking Code Faster Than You Write It

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 Your Tests Lie and You Believe Them

\n\n

Imagine: you wrote 50 unit tests, covered all if-else branches, ran them through CI — everything is green. A week later, production crashes with an error "Division by zero". How? The tests passed, didn't they? Sound familiar?

\n\n

The problem with classic unit tests (example-based, with specific numbers) is that they check what you already know. You choose arguments that, in your opinion, will lead to an error. But a programmer's mind is predictable: we all write testDivideByZero and forget about testDivideByNegativeZero or testDivideByVeryLargeFloat.

\n\n

This is where property-based testing in PHP comes into play — a method that doesn't check specific scenarios but proves properties of your code for thousands of random inputs. Today, we'll break down how to implement this approach in your PHP project using Eris and PHPUnit. You'll learn how to find bugs that regular tests miss and why this isn't magic, but engineering.

\n\n

What is Property-based Testing? (And Why It's Not Random)

\n\n

A regular unit test looks like this: assert(2 + 2 === 4). You check one specific assertion. Property-based testing (PBT) works differently: you formulate a property — an assertion that must be true for any valid input data.

\n\n

Example of a property for an addition function:

\n
    \n
  • Commutativity: a + b === b + a for any numbers.
  • \n
  • Idempotence: repeated application of the function does not change the result.
  • \n
  • Invariance: the result is always positive (if that's the property).
  • \n
\n\n

PBT generates hundreds of random values (including edge cases, zeros, negatives, NaN) and checks whether the property is violated. If a counterexample is found, the test fails with the specific input data that caused the failure. You don't just get "test failed," but a reproducible bug.

\n\n

In PHP, this technique is especially useful due to dynamic typing: you never know what type will actually be passed to a function.

\n\n

Tools for Property-based Testing in PHP

\n\n

The main tool is the Eris library (github.com/giorgiosironi/eris). It integrates with PHPUnit and provides random data generators. An alternative is the built-in capabilities of PHPUnit 10+ (but they are still somewhat immature). We'll use Eris, as it is a mature and well-documented tool.

\n\n

Installation via Composer:

\n
composer require --dev giorgiosironi/eris
\n\n

Faker will also come in handy for generating complex structures (emails, names, addresses).

\n\n
composer require --dev fakerphp/faker
\n\n

Writing Your First Property-based Test: Breaking Sorting

\n\n

Let's say we have a function sortArray() that uses quicksort. Let's write a property: "the array after sorting should not contain elements smaller than the previous one."

\n\n
use Eris\\TestTrait;\nuse Eris\\Generator\\IntegerGenerator;\nuse PHPUnit\\Framework\\TestCase;\n\nclass SortTest extends TestCase\n{\n    use TestTrait;\n\n    public function testSortedArrayProperty(): void\n    {\n        $this->forAll(\n            Generator\\seq(Generator\\int())\n        )->then(function (array $input) {\n            $sorted = sortArray($input);\n            for ($i = 0; $i < count($sorted) - 1; $i++) {\n                $this->assertLessThanOrEqual(\n                    $sorted[$i + 1],\n                    $sorted[$i],\n                    "Array is not sorted: " . json_encode($sorted)\n                );\n            }\n        });\n    }\n}\n
\n\n

Running

Blogs

Book Recommendations