Недавно увидел Testo — новый фреймворк для тестов на PHP.
Если в проекте уже используются Pest-тесты, стоит ли смотреть в сторону Testo и тем более переезжать?
Короткий ответ: с работающего Pest-проекта переходить пока не нужно. Testo интересный, но это не замена Pest в один клик.
Что такое Testo
Testo — самостоятельный фреймворк для тестирования PHP. Он не работает поверх PHPUnit, а запускает тесты своим раннером.
Тест выглядит так:
use Testo\Assert;
use Testo\Test;
#[Test]
final class ActivityTest
{
public function restoresSoftDeletedActivity(): void
{
$activity = Activity::factory()->create();
$activity->delete();
$activity->restore();
Assert::false($activity->trashed());
}
}
То есть обычный PHP-класс, методы и атрибут #[Test]. Не нужно наследоваться от TestCase.
У Testo есть плагины для data provider’ов, повторного запуска тестов, покрытия кода, бенчмарков, inline-тестов. Есть интеграции с Mockery и Infection. Конфигурация хранится в PHP-файле testo.php, а не в XML. Подробности есть в документации Testo и на Packagist.
Чем Testo отличается от PHPUnit
PHPUnit — главный и самый старый тестовый фреймворк в PHP-мире. Laravel из коробки использует PHPUnit, Pest тоже работает на его базе.
Классический PHPUnit-тест выглядит так:
use PHPUnit\Framework\TestCase;
final class ActivityTest extends TestCase
{
public function testCanRestoreSoftDeletedActivity(): void
{
$activity = new Activity();
$activity->delete();
$activity->restore();
$this->assertFalse($activity->trashed());
}
}
Testo предлагает другой подход:
use Testo\Assert;
use Testo\Test;
#[Test]
final class ActivityTest
{
public function canRestoreSoftDeletedActivity(): void
{
$activity = new Activity();
$activity->delete();
$activity->restore();
Assert::false($activity->trashed());
}
}
PHPUnit — большая зрелая экосистема с огромным количеством интеграций. Testo — отдельный молодой инструмент со своей архитектурой и набором плагинов.
А Pest тут где?
Pest — это удобный синтаксис поверх PHPUnit.
test('can restore soft deleted activity', function () {
$activity = Activity::factory()->create();
$activity->delete();
$activity->restore();
expect($activity->trashed())->toBeFalse();
});
Для Laravel это очень удобный вариант. Тесты короткие, читаются нормально, а вся база PHPUnit остаётся доступной.
Важно: Testo не поддерживает Pest-синтаксис из коробки. Такой код:
test('can restore soft deleted activity', function () {
// ...
});
не получится просто запустить через Testo.
У Testo свой стиль: классы, методы, атрибуты и Assert.
Но миграция частично автоматизируется
Для перехода есть пакет testo/bridge-rector. Это набор правил для Rector, который умеет конвертировать тесты между PHPUnit, Pest и Testo.
Например, он может заменить Pest-проверки:
expect($activity->trashed())->toBeFalse();
на проверки Testo:
Assert::false($activity->trashed());
Для этого в rector.php подключается набор правил:
use Rector\Config\RectorConfig;
use Testo\Bridge\Rector\Set\TestoRectorSetList;
return RectorConfig::configure()
->withPaths([__DIR__ . '/tests'])
->withSets([
TestoRectorSetList::PEST_TO_TESTO,
]);
Но есть важный нюанс. Pest-тест — это функция с замыканием, а Testo-тест — метод класса. Автоматически превратить:
test('can restore soft deleted activity', function () {
// ...
});
в полноценный класс с методом нельзя без потери смысла или странных решений.
Поэтому мигратор конвертирует assertions, но структуру тестов из Pest в классы нужно будет привести вручную. Для PHPUnit ситуация лучше: классы и методы уже есть, поэтому переход на Testo автоматизируется заметно проще.
Инструмент полезный, но запускать его сразу на всём проекте не стоит. Сначала лучше выбрать одну папку с тестами, прогнать Rector, посмотреть diff и запустить полный набор тестов.
Нужно ли переходить с Pest на Testo
В обычном Laravel-проекте — нет.
Если у вас уже есть Pest:
- тесты запускаются в CI;
- используются Laravel factories,
RefreshDatabase,actingAs(); - команда понимает текущий код;
- есть свои хелперы, datasets и моки;
то переезд даст много работы и мало пользы.
Даже с автоматической заменой assertions нужно проверить интеграцию с Laravel, миграции, database-тесты, coverage, отчёты CI, моки и запуск из PhpStorm.
Тесты нужны, чтобы ловить ошибки. Если во время миграции вы неделю ловите ошибки в самих тестах — задача начинает выглядеть сомнительно.
Когда Testo стоит попробовать
Testo можно взять для нового небольшого пакета или отдельного модуля с чистой бизнес-логикой.
Например:
- расчёт скидок;
- правила начисления бонусов;
- конвертеры данных;
- SDK-клиент;
- библиотека, не завязанная на Laravel.
Там можно понять, нравится ли подход: насколько удобно писать тесты, запускать их локально, смотреть ошибки и работать с данными.
Если всё-таки переходить — как сделать это без боли
Не нужно переписывать все тесты одним махом.
Лучший путь такой:
- Оставить Pest и PHPUnit как есть.
- Выбрать небольшой изолированный модуль.
- Подключить
testo/bridge-rectorи прогнать его только по этому модулю. - Вручную перевести функциональные Pest-тесты в классы и методы Testo.
- Запустить тесты локально и в CI.
- Посмотреть, удобно ли с ним жить хотя бы пару недель.
Если Testo реально оказался удобнее именно в вашем коде — можно использовать его в новых независимых пакетах. Старые Pest-тесты при этом трогать не нужно.
Вывод
Если вы используете Pest в Laravel-проекте — оставайтесь на Pest. Он даёт удобный синтаксис, дружит с Laravel и остаётся частью большой PHPUnit-экосистемы.
Testo стоит попробовать в новом небольшом проекте, если интересно посмотреть на тесты без PHPUnit и с более явной PHP-конфигурацией.
Миграция частично автоматизируется через Rector: assertions он переведёт, но Pest-функции в классы Testo придётся переносить вручную. Поэтому переход имеет смысл только если Testo решает вашу конкретную задачу, а не просто потому, что появился новый фреймворк.