Skip to content
...

Laravel (Laratesto) ​

Laratesto помогает тестировать Laravel-приложения на Testo. Пакет поднимает новое приложение для каждого теста и даёт знакомые по тестам Laravel хелперы: HTTP-запросы, проверки аутентификации, сессии и базы данных, фасады с их фейками и Artisan-команды.

Сторонний пакет

Laratesto разрабатывается и поддерживается сторонним разработчиком ichinya. Сообщать об ошибках и следить за релизами можно в его репозитории.

Класс плагина: LaravelPlugin\Laratesto\LaravelPlugin. Не входит в состав плагинов по умолчанию. GitHub

Установка ​

Для Laratesto нужны приложение на Laravel 13 и PHP 8.3 или новее. Выполните в корне приложения:

bash
composer require --dev ichinya/laratesto

Подходящую версию Testo Composer установит вместе с пакетом. Переходить можно постепенно: существующие тесты на PHPUnit или Pest продолжат работать со своим раннером.

Настройка ​

Зарегистрируйте LaravelPlugin\Laratesto\LaravelPlugin в секции plugins того Test Suite, тестам которого нужно запущенное приложение Laravel. Обычно это feature-тесты. Юнит-тесты лучше держать в отдельном Test Suite без плагина, чтобы приложение не поднималось ради каждого из них. Если Testo в проекте ещё не настроен, начните со страницы Начало работы.

php
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
<?php

use Illuminate\Support\Facades\Route;

Route::get('/laratesto/ping', static fn() => response()->json(['message' => 'pong']));

И тест для него в tests/Feature/HttpTest.php:

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
<?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Защищённый хук после каждого теста, пока приложение ещё доступно.. Один тест проходит так:

  1. Laratesto загружает приложение и передаёт его в тест.
  2. Если у теста есть атрибут для БД, база подготавливается.
  3. Вызывается setUpLaravel().
  4. Выполняется сам тест.
  5. Вызывается tearDownLaravel().
  6. Транзакция или миграции откатываются, и 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
<?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()]Явно помечает метод, функцию или класс как тест. на тестовые методы.

bash
composer require --dev ichinya/laratesto-rector

Перед миграцией закоммитьте тесты: если в файлах есть незакоммиченные правки, мигратор откажется их менять. Затем запустите его дважды: сначала для предпросмотра, потом чтобы применить изменения.

bash
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.