Тестирование
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.
Антипаттерны: тесты, зависящие от порядка выполнения; общая БД без очистки; моки «всего подряд» — тест проверяет моки, а не поведение.