Почему 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 дней гарантии