Тестирование

Feature и Unit тесты, RefreshDatabase, фабрики, моки и фейки, Pest.

Junior Какие виды тестов есть в Laravel и чем Feature отличается от Unit?
  • Feature-тесты (tests/Feature) — тестируют приложение «снаружи»: HTTP-запрос → ответ, БД, сессии. Покрывают целые сценарии.
  • Unit-тесты (tests/Unit) — изолированная логика класса без фреймворка (не наследуют Tests\TestCase или наследуют базовый PHPUnit TestCase).
class PostTest extends TestCase
{
    public function test_guest_cannot_create_post(): void
    {
        $this->postJson('/api/posts', ['title' => 'X'])
            ->assertUnauthorized();              // 401
    }

    public function test_user_can_create_post(): void
    {
        $user = User::factory()->create();

        $this->actingAs($user)
            ->postJson('/api/posts', ['title' => 'Hello'])
            ->assertCreated()
            ->assertJsonPath('title', 'Hello');

        $this->assertDatabaseHas('posts', ['title' => 'Hello']);
    }
}

Полезные утверждения: assertOk/ assertStatus, assertJson, assertJsonStructure, assertRedirect, assertSessionHas, assertDatabaseHas/Missing, assertViewHas, assertSee.

Запуск: php artisan test (обёртка над PHPUnit с параллелизмом --parallel).

Middle Что делает RefreshDatabase и как настроить тестовое окружение?

RefreshDatabase — трейт, который перед тестами накатывает миграции и оборачивает каждый тест в транзакцию (откат после) — тесты изолированы и быстрые:

class OrderTest extends TestCase
{
    use RefreshDatabase;

    public function test_order_created(): void
    {
        Order::factory()->create(['total' => 100]);
        $this->assertDatabaseCount('orders', 1);
    }
}

Настройка окружения — phpunit.xml:

<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="CACHE_STORE" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="MAIL_MAILER" value="array"/>

In-memory SQLite — мгновенные тесты; для MySQL-специфики — отдельная тестовая БД.

Альтернативы: DatabaseMigrations (migrate:fresh на каждый тест — медленно), DatabaseTransactions (без миграций, если БД уже есть).

Фабрики — стандарт подготовки данных; setUp() — общая подготовка; withoutExceptionHandling() — увидеть реальный exception вместо ответа 500.

Middle Как тестировать очереди, события, почту и внешние сервисы?

Laravel предлагает фейки — заменяют инфраструктуру на «заглушки» с утверждениями:

Bus::fake();
Bus::assertDispatched(ProcessPodcast::class, fn ($job) => $job->podcast->id === 1);
Bus::assertNotDispatched(OtherJob::class);

Queue::fake();
Queue::assertPushed(ProcessPodcast::class);

Event::fake();
Event::assertDispatched(OrderShipped::class);

Mail::fake();
Mail::assertSent(InvoicePaid::class, fn ($m) => $m->hasTo('user@example.com'));

Notification::fake();
Notification::assertSentTo($user, InvoicePaid::class);

Storage::fake('s3');
Storage::disk('s3')->assertExists("avatars/{$file->hashName()}");

Внешние HTTP — фейк клиента:

Http::fake(['api.example.com/*' => Http::response(['ok' => true], 200)]);
Http::assertSent(fn ($r) => $r->url() === 'https://api.example.com/pay');

Кастомные сервисы — моки через контейнер:

$this->mock(PaymentGateway::class, fn ($m) => $m->shouldReceive('charge')->andReturnTrue());
$this->instance(Cache::class, new ArrayStore);
Middle Что такое Pest и чем он отличается от PHPUnit?

Pest — популярный тестовый фреймворк поверх PHPUnit с функциональным синтаксисом (стиль Jest):

test('guest cannot see dashboard', function () {
    $this->get('/dashboard')->assertRedirect('/login');
});

it('creates a post', function () {
    actingAs($user)->post('/posts', ['title' => 'Hi']);

    expect(Post::count())->toBe(1)
        ->and(Post::first()->title)->toBe('Hi');
})->uses(RefreshDatabase::class);

Особенности:

  • цепочечные expect()-утверждения с читаемыми матчерами;
  • datasets — параметризованные тесты: ->with(['draft', 'published']);
  • beforeEach, группировка describe, архитектурные тесты (arch() — например, «контроллеры не используют модели напрямую»);
  • плагины: coverage, parallel, snapshots.

Работает поверх PHPUnit — совместим с ним в одном проекте. На собеседовании достаточно знать оба и что выбор — вкус команды: Pest лаконичнее, PHPUnit стандартнее.

Senior Как подходить к тестированию приватных методов, времени и случайности?

Приватные методы не тестируют напрямую — только через публичный контракт. Если приватная логика сложная и требует теста — это сигнал вынести её в отдельный класс (сервис/стратегию) и тестировать публично.

Время — замораживается:

$this->travelTo(now()->startOfDay());          // Laravel
$this->travel(5)->minutes();
Carbon::setTestNow('2026-01-01 12:00:00');     // Carbon

Случайность — детерминируется:

Str::createUuidsUsingSequence([$uuid1, $uuid2]);
fake()->seed(1234);

Другие приёмы:

  • Http::fakeSequence() — разные ответы на повторные вызовы;
  • состояния фабрик вместо дублирования (User::factory()->admin());
  • assertDatabaseHas вместо сравнения целых моделей;
  • параллельный запуск: php artisan test --parallel;
  • coverage: php artisan test --coverage --min=80.

Антипаттерны: тесты, зависящие от порядка выполнения; общая БД без очистки; моки «всего подряд» — тест проверяет моки, а не поведение.