Skip to content

Тестирование Laravel — лучшие практики

Дополняет внутренний ../testing.md (стратегия, защита рабочей БД). КРИТИЧНО про тестовую БД (RefreshDatabase только на *_test) — см. ../testing.md.

Pest vs PHPUnit

Laravel 11+ ставит Pest по умолчанию. Оба валидны; Pest — лаконичнее, PHPUnit — классика.

php
// Pest
it('validates phone on contact form', function () {
    $response = $this->post(route('contact.store'), ['phone' => 'invalid']);
    $response->assertSessionHasErrors('phone');
});

// PHPUnit-эквивалент
public function test_contact_form_validates_phone(): void
{
    $this->post(route('contact.store'), ['phone' => 'invalid'])
        ->assertSessionHasErrors('phone');
}

Пирамида тестов

ТипДоляЧто
FeatureбольшинствоHTTP, роуты, формы, API, редиректы — основной фокус
Unitкритичная логикаService с расчётами/ветвлением, formulas, edge cases
Browser (Pest v4 / Dusk)минимумключевые JS-сценарии, финальная проверка

Не писать unit на CRUD без логики (покрывается feature) и на Eloquent-связи (ответственность фреймворка).

Factories — основа тестовых данных

php
class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'title' => fake()->sentence(),
            'status' => PostStatus::Draft,
            'author_id' => User::factory(),
        ];
    }

    public function published(): static    // state
    {
        return $this->state(['status' => PostStatus::Published, 'published_at' => now()]);
    }
}

// В тесте:
Post::factory()->count(3)->published()->for($user, 'author')->create();
  • Каждая модель → factory (правило из чеклиста коммита ../../CLAUDE.md).
  • States для вариаций, relationships через for()/has().

Fakes — изоляция побочных эффектов

php
Mail::fake(); Queue::fake(); Event::fake(); Notification::fake(); Storage::fake('s3');
Http::fake(['api.example.com/*' => Http::response(['ok' => true], 200)]);

// Потом:
Mail::assertSent(OrderConfirmation::class);
Queue::assertPushed(ProcessImport::class);
Http::assertSent(fn ($r) => $r->url() === 'https://api.example.com/charge');

Внешние зависимости (mail, queue, API, telegram) — мокать. НЕ мокать БД (использовать RefreshDatabase с реальными запросами — иначе тесты проходят, а миграция/SQL сломаны).

Что тестировать обязательно (Feature)

Чеклист для нового роута (из ../testing.md):

  1. GET → 200, наличие <title>, <h1>
  2. GET несуществующий slug → 404
  3. POST валидные данные → success (201/302)
  4. POST невалидные → 422 / redirect с errors
  5. 301 redirect → правильный destination
  6. API rate limit → 429 после лимита
  7. Sitemap содержит все сущности из БД, все URL → 200

Принципы

  • Один assert = одно поведение (понятно, что сломалось).
  • Тестировать поведение, не реализацию — рефакторинг не должен ломать тест.
  • Изоляция — тест не зависит от порядка/других тестов (flaky иначе).
  • Именование: test_{что}_{ожидание}test_guest_cannot_access_admin.
  • AAA: Arrange (factory/seed) → Act (запрос) → Assert.

Запуск (через Docker)

bash
docker exec {container} php artisan test
docker exec {container} php artisan test --filter=ContactFormTest
docker exec {container} php artisan test --parallel       # параллельно (отдельные test-БД на процесс)
docker exec {container} php artisan test --coverage --min=70

L11+ ускоряет :memory: SQLite-тесты (переиспользование PDO — до 2x). Но для проектов с PostGIS/jsonb — тестовая БД = тот же PostgreSQL с extensions (см. ../testing.md).

CI-связка

pint --test          # стиль
larastan (phpstan)   # статический анализ
php artisan test     # тесты
composer audit       # CVE

Всё зелёное → merge. См. чеклист коммита в ../../CLAUDE.md.

Anti-patterns

ПаттернПроблемаРешение
RefreshDatabase на рабочей БДпотеря всех данныхDB_DATABASE={project}_test в phpunit.xml
Mock БДмиграция/SQL сломаны, а тест зелёныйRefreshDatabase + реальные запросы
Тест зависит от порядкаflakyизолировать каждый
10 assert в одном тестенепонятно что упалоодин assert = одно поведение
Тест проверяет внутренностиломается при рефакторингетестировать поведение
Нет теста на публичный URLсломанная страница в prodfeature-тест на каждый роут
Реальный вызов внешнего APIflaky, медленно, платноHttp::fake()

Внутренняя база знаний студии