Property-based тестирование в PHP: как находить баги, о которых вы даже не догадывались
Вы когда-нибудь чувствовали, что ваши юнит-тесты — это как фонарик в тёмной комнате: подсвечивают только то, на что вы сами направили луч? Вы пишете тесты для очевидных случаев, проверяете граничные значения, но всё равно находите баги в продакшене. Знакомо?
Добро пожаловать в мир property-based тестирования в PHP. Это не просто очередная модная методология. Это смена парадигмы: вместо того чтобы придумывать конкретные примеры (example-based), вы описываете свойства, которым должен удовлетворять ваш код, и позволяете компьютеру генерировать тысячи случайных сценариев.
В этой статье мы разберём, как property-based тестирование в PHP помогает находить скрытые дефекты, которые пропускают классические тесты. Вы увидите реальные примеры кода, узнаете про библиотеку Eris и поймёте, почему этот подход становится стандартом в серьёзных проектах.
Что такое property-based тестирование?
Термин пришёл из функционального программирования (спасибо Haskell и библиотеке QuickCheck), но идея универсальна. Вместо того чтобы писать:
public function testAddition(): void
{
$this->assertSame(4, add(2, 2));
$this->assertSame(0, add(-1, 1));
$this->assertSame(10, add(7, 3));
}
Вы формулируете свойство (property): «Для любых целых чисел $a и $b результат сложения add($a, $b) должен быть равен $b + $a (коммутативность)». Фреймворк генерирует сотни случайных пар и проверяет это свойство.
Разница колоссальная: вы не ограничены своей фантазией. Машина найдёт такие комбинации, которые вы бы никогда не придумали. Например, что случится, если передать очень большое число, ноль, отрицательное значение или null?
Property-based vs Example-based: ключевые отличия
- Example-based: вы пишете конкретный вход и ожидаемый выход. Быстро, понятно, но покрывает только то, что вы предусмотрели.
- Property-based: вы описываете инварианты (свойства) и используете генераторы случайных данных. Покрывает неизвестные сценарии, но требует более абстрактного мышления.
Идеальный подход — комбинировать оба: example-based для документирования типичных случаев, property-based для стресс-тестирования и поиска граничных ошибок.
Почему property-based тестирование в PHP — это must-have?
PHP — динамический язык с нестрогой типизацией. Это значит, что ваша функция может получить на вход что угодно: строку вместо числа, объект вместо массива, null там, где ожидается int. Property-based тестирование в PHP отлично выявляет:
- Неявные assumption — «я думал, сюда всегда приходит положительное число».
- Проблемы с типами — функция ломается на float, хотя ожидает int.
- Пограничные значения — переполнение, пустые массивы, отрицательные индексы.
- Нарушение контрактов — функция не сохраняет иммутабельность или возвращает неожиданный тип.
Особенно это ценно для образовательных проектов и библиотек, где пользователи могут передавать любые данные. Если вы пишете код для других, property-based тесты — ваша страховка от глупых ошибок.
Библиотека Eris: инструмент для property-based тестирования в PHP
В экосистеме PHP есть несколько библиотек, но самая зрелая — Eris (форк оригинальной Eris от Giorgio Sironi). Она интегрируется с PHPUnit и предоставляет мощные генераторы и шринкеры (shrinking — автоматическое уменьшение примера до минимального, воспроизводящего ошибку).
Установка через Composer:
composer require --dev eris/eris
Первый тест с Eris
Допустим, у нас есть функция reverseArray, которая переворачивает массив. Какие свойства мы можем проверить?
- Инволюция: Если применить reverse дважды, получим исходный массив.
- Длина: Длина массива не меняется.
- Сохранение элементов: Все элементы остаются, просто меняют порядок.
Вот как это выглядит с Eris:
<?php
use Eris\Generator;
use Eris\TestTrait;
use PHPUnit\Framework\TestCase;
class ArrayPropertyTest extends TestCase
{
use TestTrait;
public function testReverseIsInvolution(): void
{
$this->forAll(
Generator\seq(Generator\int())
)
->then(function (array $array): void {
$reversed = array_reverse($array);
$doubleReversed = array_reverse($reversed);
$this->assertSame($array, $doubleReversed);
});
}
public function testReversePreservesLength(): void
{
$this->forAll(
Generator\seq(Generator\int())
)
->then(function (array $array): void {
$originalCount = count($array);
$reversed = array_reverse($array);
$this->assertCount($originalCount, $reversed);
});
}
}
Запустите тест, и Eris сгенерирует 100 случайных массивов (по умолчанию). Если какой-то упадёт, фреймворк покажет минимальный пример, ломающий тест. Например, пустой массив или массив с одним элементом.
Более сложный пример: тестирование сортировки
Теперь представьте, что вы написали свою реализацию сортировки (например, пузырьковую). Property-based тестирование в PHP поможет проверить, что она действительно работает корректно.
Свойства правильно отсортированного массива:
- Отсортированность: Каждый следующий элемент больше или равен предыдущему.
- Перестановка: Множество элементов не изменилось (сортируем элементы, а не выбрасываем).
- Идемпотентность: Если массив уже отсортирован, повторная сортировка не меняет его.
Код:
<?php
use Eris\Generator;
use Eris\TestTrait;
use PHPUnit\Framework\TestCase;
class SortPropertyTest extends TestCase
{
use TestTrait;
public function testBubbleSortProducesSortedArray(): void
{
$this->forAll(
Generator\seq(Generator\int())
)
->then(function (array $array): void {
$sorted = $this->bubbleSort($array);
// Проверка, что каждый элемент <= следующего
for ($i = 0; $i < count($sorted) -