Что быстрее
С самой бета-версии Testo меня спрашивали, сравнивал ли я его по скорости с PHPUnit. Я отвечал, что нет, и обычно добавлял: скорее всего, чуть медленнее. В Testo на каждую фичу строится пайплайн с мидлварями, а за гибкость и удобство принято платить производительностью.
Руки до замеров не доходили. А тут выдался повод.
Повод
На днях Brent опубликовал твит, в котором его Tempest Testing десятикратно уделывает PHPUnit: примерно 3 секунды против 30.
Кодовая база одна и та же, тесты те же. Различаются только фреймворки.
Первое, что приходит в голову — распараллелил. Но нет: в твите написано, что всё запускается в один поток.
Тогда файберы? Tempest уже асинхронный? Это можно проверить: кодовая база открыта — brendt/stitcher.io, ветка testing.
А раз есть проект, где два раннера уже стоят рядом на одинаковых тестах, самое время добавить третий и посмотреть, какое место Testo займёт между асинхронным Tempest и последовательным PHPUnit.
Стенд
PHPUnit я вернул в проект из основной ветки — в отдельную папку рядом с тестами Tempest, и сделал ещё одну папку для Testo. Тесты из PHPUnit в Testo перегнал AI-агент: для этого у Testo есть скиллы и правила Rector, которые конвертируют тесты между фреймворками.
Попутно пришлось восстанавливать окружение и отправить фикс в Tempest Testing, потому что проект тупо не запускался.
Инфраструктуру я тоже подкрутил. На MySQL тесты идут очень медленно — две минуты на Tempest, поэтому переключил всё на SQLite. А вместо Redis взял сразу Valkey.
Первые прогоны
- PHPUnit — 5 минут 22 секунды.
- Tempest — около 12 секунд.
- Testo — около 17 секунд.
Отставание PHPUnit выглядело слишком большим, особенно в сравнении с Testo. Пошёл разбираться и кое-что обнаружил:
- В PHPUnit на каждый тест пересобирался весь Kernel фреймворка. В Tempest вместо этого сбрасывалось состояние уже собранного. Одна эта правка сократила время с 5 минут до 30 секунд.
- Атрибут
#[Before]висел наsetUp(), который и так работает какBefore. Смешно, но, похоже, PHPUnit в таком случае дёргаетsetUp()дважды. Минус ещё четверть времени.
Файберов в Tempest я, кстати, так и не нашёл. Весь его буст объяснялся более удачной обвязкой вокруг тестов, а не асинхронностью.
Результаты
Все механизмы, влияющие на время (сброс состояния, миграции, хаки над шиной событий), я посинкал между тремя раннерами. Вот что получилось:
| раннер | процессов | круг 1 | круг 2 | среднее |
|---|---|---|---|---|
| tempest | 1 | 24.5с | 24.3с | 24.4с |
| tempest | 5 (дефолт) | 12.5с | 12.3с | 12.4с |
| testo | 1 | 21.1с | 21.2с | 21.1с |
| phpunit | 1 | 22.5с | 22.6с | 22.5с |
Честно говоря, я удивлён, что Testo в одном потоке оказался быстрее всех. Tempest в дефолтных пяти процессах ожидаемо ускоряется почти вдвое относительно своего однопоточного прогона. Параллельный запуск появится и в Testo: он точно входит в план на 1.0.
Куда интереснее другое число. По метрикам Testo выполнение самих тестовых функций заняло меньше секунды на весь прогон. Вычитаем 1 секунду на сам тестовый фреймворк и получается, что остальные двадцать с лишним секунд — это обвязка: поднять приложение, накатить миграции, сбросить состояние между тестами...
Совет
Раннер всего лишь отвечает за то, чтобы найти тесты, вызвать их и собрать результаты. Поэтому прежде чем менять раннер ради скорости, стоит посмотреть, чем занят ваш setUp().
