Причины нестабильности тестов PHPUnit (Flaky tests)

Иногда PHPUnit тесты выполняются не с первого раза. Работают через раз. Или, например, каждый 10-й запуск фейлится. Является называется Flaky tests.

Запуск одного Flaky test

Вот однострочник, который запускает один тест InjectOfficeTest::testOfficeNameRouteToOffice в бесконечном режиме до тех пор, пока он не упадет. Также выводит количество раз запуска и потраченное время.

# Предварительно настроить alias, если его нет:
# alias atf='php artisan test --filter'
 
start=$(date +%s); count=0; while atf InjectOfficeTest::testOfficeNameRouteToOffice; do ((count++)); done; echo "Executed $count times in $(( $(date +%s) - start )) seconds"

Кейсы, которые у меня были

1. Нарушение уникальности в БД.

Лечение: добавить unique():

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

2. webp

$name = sprintf('%s.%s', $this->faker->uuid(), $this->faker->fileExtension());
$file = UploadedFile::fake()->image($name); // тут ошибка

Иногда фейлился из-за отсутствия imagewebp в образе PHP. Причем в workspace это расширение стоит.

Лечение:

$name = sprintf('%s.jpg', $this->faker->uuid());

3. tearDown

Был кейс, когда идет уборка в tearDown. Но из-за какой-то ошибки необходимая уборка не вызывалась. Поэтому после первого раза переставало работать.

4. Storage::fake()

Storage::fake() прибирается не в конце теста, а наоборот в начале. Это может повлиять на соседний тест или повторный запуск.

5. dataProvider

PHPUnit перед тем как реально запускать тесты, обходит все тестовые файлы, а в них - запускает все dataProvider, чтобы составить карту запуска. Это первый проход. На основе его как раз и работает --filter.

У меня был кейс, когда dataProvider менял состояние приложения, и поэтому работало через раз.

6. register_shutdown_function, destructor() контроллера

register_shutdown_function() запускается, когда свою работу завершает не только сам тест, но и PHPUnit. Деструктор контроллера также запускается слишком поздно.

Если в них разместить теструемую логику, тест вообще никогда не пройдет.

Решение:

public function __construct()
{
    CacheClearJob::dispatch()->afterResponse();
}

7. Небольшая разница при сравнении времени

Возникает при сравнении дат $this->assertEquals(), часто равна 1 секунде. Объясняется, что время движется.

Решение - зафиксировать время:

Carbon::setTestNow(now()->timezone('Europe/Moscow')->floorSecond());
Carbon::setTestNow(now());

Пишите еще кейсы, с которыми сталкивались.

Печать/экспорт