Introduction: Why Your Tests Lie and You Believe Them
\n\nImagine: 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?
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.
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\nWhat is Property-based Testing? (And Why It's Not Random)
\n\nA 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.
Example of a property for an addition function:
\n- \n
- Commutativity:
a + b === b + afor 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
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\nIn PHP, this technique is especially useful due to dynamic typing: you never know what type will actually be passed to a function.
\n\nTools for Property-based Testing in PHP
\n\nThe 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\nInstallation via Composer:
\ncomposer require --dev giorgiosironi/eris\n\nFaker will also come in handy for generating complex structures (emails, names, addresses).
\n\ncomposer require --dev fakerphp/faker\n\nWriting Your First Property-based Test: Breaking Sorting
\n\nLet'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."
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\nRunning