Feature-тести (інтеграційні тести) потрібні не “для галочки”, а щоб ти кожного разу не перевіряв модуль руками. Вони імітують реальні дії користувача: зайшов на сторінку списку, відкрив форму, відправив POST, отримав редирект, а запис реально з’явився в базі. Навіть 2–3 тести “happy path” дають різкий приріст надійності: ти одразу бачиш, що маршрути, валідація, контролер і запис у БД працюють разом.
У Laravel найпростіший старт — це php artisan test, а в самих тестах — RefreshDatabase, щоб база для тестів була чистою. Під “happy path” мається на увазі сценарій без помилок: користувач має доступ, відправляє коректні дані, отримує очікувану відповідь. Для першого модуля (Tags/Sections/Resources) цього достатньо, щоб закрити головний ризик: “маршрути не туди ведуть” або “нічого не записується”.
Найкращий набір із 2–3 тестів для CRUD-модуля виглядає так. Перший тест — index відкривається і повертає 200 (або робить редирект на логін, якщо гість). Другий тест — store створює запис: відправили POST з валідними даними, отримали редирект (зазвичай 302) і в базі з’явився рядок. Третій тест — update змінює запис: відправили PUT/PATCH, отримали редирект і в базі оновилось поле. Це найчистіший “скелет” перевірки реального потоку.
Нижче — практичний шаблон під модуль Tags. Навіть якщо ти робиш Resources або Sections, логіка однакова: змінюються лише таблиця, маршрути і поля. Важлива дисципліна: у тесті ти звертаєшся до named routes (route('admin.tags.index')), а не до “жорстких” URL, тоді тести не зламаються, якщо завтра ти змінюватимеш шлях.
php artisan make:test TagRoutesTest
namespace Tests\Feature\Admin;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\User;
use App\Models\Tag;
class TagRoutesTest extends TestCase
{
use RefreshDatabase;
protected function adminUser(): User
{
// Варіант 1 (кращий): factory, якщо у вас вони є
// return User::factory()->create();
// Варіант 2 (якщо factory немає): створити мінімального юзера руками
return User::query()->create([
'name' => 'Test Admin',
'email' => 'admin@test.local',
'password' => bcrypt('password'),
]);
}
public function test_index_page_opens_for_authorized_user(): void
{
$user = $this->adminUser();
// Якщо у вас жорсткий permission middleware — тут треба дати юзеру права.
// Це залежить від вашої реалізації (roles/permissions). Ідея: видати доступ перед запитом.
$resp = $this->actingAs($user)->get(route('admin.tags.index'));
$resp->assertStatus(200);
// Додатково можна перевірити, що сторінка містить заголовок або кнопку "Створити"
// $resp->assertSee('Теги');
}
public function test_store_creates_tag_and_redirects(): void
{
$user = $this->adminUser();
$payload = [
'name' => 'Laravel',
'slug' => 'laravel',
];
$resp = $this->actingAs($user)->post(route('admin.tags.store'), $payload);
$resp->assertStatus(302);
$resp->assertRedirect(route('admin.tags.index'));
$this->assertDatabaseHas('tags', [
'name' => 'Laravel',
'slug' => 'laravel',
]);
}
public function test_update_changes_tag_and_redirects(): void
{
$user = $this->adminUser();
// Якщо є factory:
// $tag = Tag::factory()->create(['name' => 'PHP', 'slug' => 'php']);
// Якщо factory немає:
$tag = Tag::query()->create(['name' => 'PHP', 'slug' => 'php']);
$payload = [
'name' => 'PHP 8',
'slug' => 'php-8',
];
$resp = $this->actingAs($user)->put(route('admin.tags.update', $tag), $payload);
$resp->assertStatus(302);
$resp->assertRedirect(route('admin.tags.index'));
$this->assertDatabaseHas('tags', [
'id' => $tag->id,
'name' => 'PHP 8',
'slug' => 'php-8',
]);
}
}
Як це запускати і що вважати “успіхом”. Запускаєш php artisan test, і тести мають проходити стабільно на чистій тестовій базі. Якщо падає 403, значить ти реально вперся в доступи — і це нормально: тест показав правду. Тоді правильне рішення не “відключити middleware назавжди”, а у helper-методі adminUser() видати потрібні permissions/role так, як це зроблено у вашому проєкті, і вже тоді повторити запит.
Якщо падає 419, значить ти тестуєш форму з CSRF/сесією некоректно або маршрут під web, але ти йдеш якось “не так”. У feature-тестах через post() і actingAs() Laravel зазвичай коректно симулює сесію, тож 419 найчастіше означає, що маршрут під іншим guard або є нестандартна перевірка. Знову ж таки, це корисно: ти бачиш, де реальна точка блокування.
Висновок тут простий: 2–3 feature-тести “happy path” — це мінімальний страховий поліс. Вони швидко ловлять поломки маршрутів, редиректів, контролерів, сервісів і БД-запису. Далі, коли модуль розростеться, ти додаєш негативні кейси (422 на валідацію, 403 на доступ), але стартовий мінімум саме такий: index відкрився, store створив, update оновив.