Yandex Metrika
sanches.free 1 просмотр

Почему PHPUnit «моргает»: flaky-тесты и что с этим делать в PHP-проекте

Что такое flaky-тест

Иногда один и тот же тест проходит не с первого раза: один прогон зелёный, следующий — нет, или падает каждый N‑й запуск. Такие сценарии называют flaky (нестабильными). Проблема не в «капризном» PHPUnit, а в скрытом состоянии окружения, общих ресурсах или порядке выполнения. Ниже — причины, с которыми чаще всего сталкиваются в проектах на PHP (в том числе рядом с 1С‑Битрикс, где тестируют кастомные сервисы, интеграции и обёртки).

Как поймать один нестабильный тест

Чтобы убедиться, что падение не случайность, удобно крутить один фильтр в цикле до первого фейла и смотреть счётчик и время. Для Laravel часто настраивают алиас на php artisan test --filter, затем в оболочке bash:

start=$(date +%s); count=0
while atf YourTest::testSomething; do ((count++)); done
echo "Executed $count times in $(( $(date +%s) - start )) seconds"

В чистом PHPUnit замените atf на вызов vendor/bin/phpunit --filter … с тем же смыслом.

Дубли в базе при генераторе данных

Фейкер может выдать повторяющееся «уникальное» значение, если не зафиксировать уникальность на уровне генератора. Тогда второй проход упирается в ограничение БД.

$this->faker->unique()->word();

Имеет смысл также проверять, что тест сам создаёт изоляцию данных или откатывает транзакции, а не опирается на «случайно свободную» строку.

Картинки, WebP и расширения PHP

Если имя файла строится через $this->faker->fileExtension(), иногда выпадает webp. UploadedFile::fake()->image() в одном окружении может требовать imagewebp, которого нет в CLI‑образе Docker, хотя в IDE/workspace расширение подключено. Практичный обход — зафиксировать расширение там, где важна воспроизводимость:

$name = sprintf('%s.jpg', $this->faker->uuid());
$file = UploadedFile::fake()->image($name);

Уборка в tearDown

Если тест падает до конца, часть логики в tearDown может не выполниться, и «грязное» состояние переходит к следующему сценарию. В итоге тест «вечно зелёный», пока не наткнётся на остатки от предыдущего прогона. Следите за симметрией подготовки и очистки: либо гарантированный откат в tearDown даже при исключениях, либо транзакционные обёртки/отдельная БД для тестов.

Storage::fake() и порядок жизненного цикла

В Laravel фейковое хранилище создаётся в начале теста и подменяет диск; соседний тест может не ожидать, что диск уже «перехвачен», или наоборот — полагаться на сброс, который произойдёт только при следующем запуске. Явно сбрасывайте состояние в setUp/tearDown или изолируйте тесты по дискам.

dataProvider и побочные эффекты

PHPUnit сначала обходит файлы и выполняет провайдеры данных, чтобы построить план запуска; фильтр --filter работает уже после этого обхода. Если провайдер меняет глобальное состояние (конфиг, кеш, таблицу), порядок обхода и кеш PHPUnit дают эффект «то работает, то нет». Провайдеры должны быть чистыми: только возврат массивов кейсов, без записи в приложение.

register_shutdown_function и деструкторы

Функции, зарегистрированные через register_shutdown_function(), и деструкторы долгоживущих объектов срабатывают после завершения теста и иногда после самого PHPUnit. Проверять там бизнес-логику бессмысленно: тест уже завершил ассерты. Если нужна отложенная работа (очередь, кеш), в вебе чаще моделируют через dispatch/afterResponse и отдельные тесты на джоб, а не на «хвост» shutdown.

Сравнение времени и «прыжок» секунды

assertEquals для двух моментов времени иногда ловит разницу в одну секунду: часы реально двигаются между двумя вызовами. Для стабильности фиксируют «сейчас» через Carbon или аналог:

Carbon::setTestNow(now()->timezone('Europe/Moscow')->floorSecond());
// или проще по проекту:
Carbon::setTestNow(now());

Не забудьте сбросить тестовое время в tearDown, чтобы не заразить соседние тесты.

Кратко

  • Flaky-тест — следствие общего состояния, а не «бага PHPUnit».
  • Проверяйте уникальность данных, расширения PHP, чистоту провайдеров и симметрию setUp/tearDown.
  • Не кладите проверяемую логику в shutdown и деструкторы — там вы уже вне контролируемой фазы теста.

Не хотите копаться сами?

Починю за 1-3 дня. Без предоплаты — оплата по результату.

15+ лет опыта с 1С-Битрикс · Без предоплаты · 7 дней гарантии