Laravel (Laratesto)
Laratesto помогает тестировать Laravel-приложения на Testo. Пакет поднимает новое приложение для каждого теста и даёт знакомые по тестам Laravel хелперы: HTTP-запросы, проверки аутентификации, сессии и базы данных, фасады с их фейками и Artisan-команды.
Сторонний пакет
Laratesto разрабатывается и поддерживается сторонним разработчиком ichinya. Сообщать об ошибках и следить за релизами можно в его репозитории.
Класс плагина: LaravelPlugin\Laratesto\LaravelPlugin. Не входит в состав плагинов по умолчанию. GitHub
Установка
Для Laratesto нужны приложение на Laravel 13 и PHP 8.3 или новее. Выполните в корне приложения:
composer require --dev ichinya/laratestoПодходящую версию Testo Composer установит вместе с пакетом. Переходить можно постепенно: существующие тесты на PHPUnit или Pest продолжат работать со своим раннером.
Настройка
Зарегистрируйте LaravelPlugin\Laratesto\LaravelPlugin в секции plugins того Test Suite, тестам которого нужно запущенное приложение Laravel. Обычно это feature-тесты. Юнит-тесты лучше держать в отдельном Test Suite без плагина, чтобы приложение не поднималось ради каждого из них. Если Testo в проекте ещё не настроен, начните со страницы Начало работы.
use Laratesto\Config\LaravelConfig;
use Laratesto\LaravelPlugin;
use Testo\Application\Config\SuiteConfig;
new SuiteConfig(
name: 'Laravel',
location: ['tests/Feature'],
plugins: [
new LaravelPlugin(new LaravelConfig(
basePath: __DIR__,
environment: 'testing',
config: [
'database.default' => 'sqlite',
'database.connections.sqlite.database' => ':memory:',
'cache.default' => 'array',
'session.driver' => 'array',
],
)),
],
);LaravelConfig
Определяет, как Laratesto загружает Laravel-приложение для каждого теста.
new LaravelConfig(string $basePath, string $environment = 'testing', array $config = [])Параметры:
$basePath- Корень приложения, то есть каталог с
bootstrap/app.php. $environment- Становится значением
APP_ENV. Laratesto выставляет его до загрузки приложения, так что Laravel, как обычно, прочитает.env.testing. $config- Переопределяет значения конфигурации. Они применяются уже после загрузки приложения, и сервис-провайдер, который читает настройку во время загрузки, их не увидит. Такие настройки задавайте в
.env.testing.
Не используйте рабочую базу
Атрибуты для работы с БД, о которых речь пойдёт ниже, запускают миграции и очищают таблицы. Подключайте тесты к базе, которую не страшно потерять, например к SQLite в памяти, как в примере (для неё нужно расширение PDO SQLite).
Пишем тесты
Наследование от базового класса
Проще всего начать с наследования от LaravelTestCase\Laratesto\Testing\LaravelTestCase. Добавим JSON-маршрут в routes/web.php:
<?php
use Illuminate\Support\Facades\Route;
Route::get('/laratesto/ping', static fn() => response()->json(['message' => 'pong']));И тест для него в tests/Feature/HttpTest.php:
<?php
declare(strict_types=1);
namespace Tests\Feature;
use Laratesto\Testing\LaravelTestCase;
use Testo\Test;
#[Test]
final class HttpTest extends LaravelTestCase
{
public function returnsJson(): void
{
$this->get('/laratesto/ping')
->assertOk()
->assertExactJson(['message' => 'pong']);
}
}При запуске теста запрос проходит через настоящее HTTP-ядро Laravel, веб-сервер для этого не нужен. В ответ приходит LaravelResponse\Laratesto\Testing\LaravelResponse: его проверки выглядят так же, как в Laravel, но засчитываются в Testo.
Трейт вместо базового класса
Если ваши тесты уже наследуют собственный базовый класс, подключите трейт InteractsWithLaravel\Laratesto\Testing\InteractsWithLaravel: он даёт классу те же хелперы. Плагин Laravel в Test Suite нужен и в этом случае. Например, tests/Feature/ContainerTest.php:
<?php
declare(strict_types=1);
namespace Tests\Feature;
use Laratesto\Testing\InteractsWithLaravel;
use Testo\Assert;
use Testo\Test;
#[Test]
final class ContainerTest
{
use InteractsWithLaravel;
public function usesTheTestingEnvironment(): void
{
Assert::same($this->app()->environment(), 'testing');
}
}Что есть под рукой
Базовый класс и трейт дают одинаковый набор защищённых хелперов:
- HTTP-запросы (
get(),postJson()и другие), свои заголовки, bearer-токены и отключение middleware. Cookies из ответа уходят со следующим запросом того же теста, как в браузере. - Аутентификация (
actingAs(),assertAuthenticated()), сессия и проверки базы данных (assertDatabaseHas(),assertModelExists()и другие). - Сервис-контейнер, фасады и их фейки (
Queue::fake(),Event::fake()). - Запуск Artisan-команд с проверкой кода выхода и вывода.
Фейки Laravel проверяют ожидания через ассерты PHPUnit. Laratesto содержит небольшую замену этих классов, поэтому фейки работают и без установленного PHPUnit. Полноценным PHPUnit она при этом не является: например, createMock() в ней нет, а для моков стоит взять Mockery. Полный список хелперов и их ограничения описаны в README Laratesto.
Жизненный цикл приложения
Каждый тест получает новый экземпляр приложения. Если для подготовки теста нужно само приложение (контейнер, конфигурация, фасады), вынесите её в хук InteractsWithLaravel::setUpLaravel()InteractsWithLaravel::setUpLaravel(): voidЗащищённый хук перед каждым тестом: приложение уже загружено, база подготовлена.. Очистку, которой тоже нужно приложение, поместите в InteractsWithLaravel::tearDownLaravel()InteractsWithLaravel::tearDownLaravel(): voidЗащищённый хук после каждого теста, пока приложение ещё доступно.. Один тест проходит так:
- Laratesto загружает приложение и передаёт его в тест.
- Если у теста есть атрибут для БД, база подготавливается.
- Вызывается
setUpLaravel(). - Выполняется сам тест.
- Вызывается
tearDownLaravel(). - Транзакция или миграции откатываются, и Laratesto сбрасывает состояние Laravel.
InteractsWithLaravel::setUpLaravel(): voidЗащищённый хук перед каждым тестом: приложение уже загружено, база подготовлена.InteractsWithLaravel::tearDownLaravel(): voidЗащищённый хук после каждого теста, пока приложение ещё доступно.Учтите, что Testo создаёт один экземпляр тестового класса и использует его для всех тестов этого класса (подробнее в Lifecycle). Конструктор вызывается только один раз, поэтому свойства, которые нужны каждому тесту заново, сбрасывайте в setUpLaravel().
По одному тесту за раз
Laravel хранит много глобального состояния, поэтому тесты приложения должны выполняться строго друг за другом. Если вы используете #[RunInFiber]#[RunInFiber(Schedule $schedule = Schedule::Solo)]Запускает тест (метод) или все тесты кейса (класс) в файберах под кооперативным планировщиком Testo., оставьте режим по умолчанию Schedule::Soloenum ScheduleКак планируются тесты Test Case под #[RunInFiber]#[RunInFiber(Schedule $schedule = Schedule::Solo)]Runs a test (on a method) or a whole case (on a class) in fibers, under Testo's cooperative scheduler..SoloПо умолчанию. Тесты выполняются последовательно по одному.. С режимами Schedule::RoundRobinenum ScheduleКак планируются тесты Test Case под #[RunInFiber]#[RunInFiber(Schedule $schedule = Schedule::Solo)]Runs a test (on a method) or a whole case (on a class) in fibers, under Testo's cooperative scheduler..RoundRobinПо одному шагу каждому тесту за раунд, по порядку. Детерминированное чередование. и Schedule::Randomenum ScheduleКак планируются тесты Test Case под #[RunInFiber]#[RunInFiber(Schedule $schedule = Schedule::Solo)]Runs a test (on a method) or a whole case (on a class) in fibers, under Testo's cooperative scheduler..RandomНа каждом шаге выбирается случайный тест. Помогает вытрясать зависимости от порядка. Без сида прогоны не воспроизводимы. тесты класса чередовались бы, поэтому Laratesto завершит их с ошибкой.
Изоляция базы данных
Трейты Laravel вроде RefreshDatabase рассчитаны на PHPUnit. С Laratesto вместо них используются одноимённые атрибуты из пространства имён Laratesto\Attribute. Любой из них можно поставить как на тестовый метод, так и на весь класс.
Если в приложении остались стандартная модель User и её миграция, тест с базой выглядит так (tests/Feature/DatabaseTest.php):
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Models\User;
use Laratesto\Attribute\RefreshDatabase;
use Laratesto\Testing\LaravelTestCase;
use Testo\Test;
#[Test]
final class DatabaseTest extends LaravelTestCase
{
#[RefreshDatabase]
public function persistsAUser(): void
{
$user = User::factory()->create();
$this->assertDatabaseHas('users', ['id' => $user->getKey()]);
}
}#[RefreshDatabase]
Один раз накатывает миграции, а каждый тест оборачивает в транзакцию с откатом.
#[RefreshDatabase(bool $seed = false, ?string $seeder = null, bool $dropViews = false, bool $dropTypes = false, ?array $connections = null)]Самый быстрый вариант для большинства приложений. migrate:fresh выполняется один раз за процесс (и повторно, если схема пропала), а дальше каждый тест работает внутри транзакции, которая после него откатывается.
#[DatabaseTransactions]
Оборачивает каждый тест в транзакцию с откатом, миграции не запускает.
#[DatabaseTransactions(?array $connections = null)]Подходит, когда схема уже готова, например если её создают вне тестов.
#[DatabaseMigrations]
Выполняет migrate:fresh перед каждым тестом и откатывает миграции после него.
#[DatabaseMigrations(bool $seed = false, ?string $seeder = null, bool $dropViews = false, bool $dropTypes = false)]Самый медленный вариант, зато каждый тест начинается с заново построенной схемы.
#[DatabaseTruncation]
Очищает таблицы перед каждым тестом.
#[DatabaseTruncation(bool $seed = false, ?string $seeder = null, bool $dropViews = false, bool $dropTypes = false, ?array $connections = null, ?array $tables = null, ?array $exceptTables = null)]Пригодится, если тестируемый код не может работать внутри транзакции. В первый раз база создаётся через migrate:fresh, а дальше перед каждым тестом таблицы очищаются и при необходимости заново заполняются сидерами. После теста ничего не убирается.
Сидеры внутри теста
Метода $this->seed() из TestCase Laravel здесь нет. Используйте параметры атрибута seed и seeder или вызовите в тесте Artisan::call('db:seed', ['--force' => true]).
Миграция с PHPUnit
У Laratesto есть отдельный пакет ichinya/laratesto-rector, который переводит существующие тесты Laravel с PHPUnit на Testo. Он построен на Rector и использует общие правила интеграции с Rector, добавляя к ним правила для Laravel. Например, он заменяет трейт RefreshDatabase одноимённым атрибутом и ставит #[Test]#[Test()]Явно помечает метод, функцию или класс как тест. на тестовые методы.
composer require --dev ichinya/laratesto-rectorПеред миграцией закоммитьте тесты: если в файлах есть незакоммиченные правки, мигратор откажется их менять. Затем запустите его дважды: сначала для предпросмотра, потом чтобы применить изменения.
php artisan laratesto:migrate-rector # предпросмотр: файлы не меняются
php artisan laratesto:migrate-rector --apply # переписать тесты
git diff -- testsПо умолчанию команда обрабатывает каталог tests. Чтобы сузить область, укажите один или несколько параметров --path. Некоторые конструкции безопасно преобразовать нельзя, например трейт БД с нестандартной настройкой. Такие места мигратор оставляет как есть, помечает комментарием laratesto-residual и перечисляет в отчёте laratesto-residuals.json. Поправьте их вручную, просмотрите diff и запустите тесты. Пустой отчёт ещё не гарантирует, что тесты ведут себя как раньше, так что настоящая проверка — их запуск.
Код выхода команды показывает результат: 2 — часть мест нужно доработать вручную, 1 — миграция не удалась.
Полный список параметров и поддерживаемых конструкций есть в руководстве по миграции.
Скиллы для AI-агентов
В Laratesto есть скиллы для AI-агентов. Устанавливаются они так же, как скиллы Testo, через llm/skills.