Property-based тестирование в PHP: ломаем код быстрее, чем вы пишете баги

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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, которая переворачивает массив. Какие свойства мы можем проверить?

  1. Инволюция: Если применить reverse дважды, получим исходный массив.
  2. Длина: Длина массива не меняется.
  3. Сохранение элементов: Все элементы остаются, просто меняют порядок.

Вот как это выглядит с 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) -                

Блоги

Рекомендаций по книгам