Кращі практики тестування в Laravel: від юніт-тестів до E2E

Коли я тільки почав працювати з Laravel, тестування здавалося мені зайвою витратою часу. “Навіщо писати тести, якщо можна просто перевірити в браузері?” — думав я наївно. Але після першого серйозного рефакторингу, коли зламалася половина функціоналу, я зрозумів цінність автоматизованого тестування. У цій статті поділюся практиками тестування в Laravel, які реально працюють і допомагають спати спокійно після деплою.

Чому тестування в Laravel критично важливе

Laravel надає потужний інструментарій для тестування прямо “з коробки”. Фреймворк побудований на PHPUnit та додає власні зручні методи для тестування HTTP-запитів, роботи з базою даних, чергами та іншими компонентами. Це не просто nice-to-have — це фундамент надійного додатка.

Є три основні рівні тестування, які я використовую у проектах: юніт-тести (перевіряють окремі класи та методи), feature-тести (тестують цілі функціонали через HTTP), та інтеграційні тести (перевіряють взаємодію компонентів). Laravel робить всі три типи тестування досить простими.

Структура тестів: організація — половина успіху

Перше, з чого варто почати — правильна структура тестів. Laravel створює дві папки: tests/Unit для юніт-тестів та tests/Feature для feature-тестів. Але в реальних проектах я створюю додаткові підпапки:

tests/
├── Feature/
│   ├── Auth/
│   │   ├── LoginTest.php
│   │   └── RegisterTest.php
│   ├── Api/
│   │   └── UserApiTest.php
│   └── Admin/
│       └── DashboardTest.php
├── Unit/
│   ├── Models/
│   │   └── UserTest.php
│   ├── Services/
│   │   └── PaymentServiceTest.php
│   └── Helpers/
│       └── DateHelperTest.php
└── TestCase.php

Така структура дозволяє швидко знайти потрібний тест і зрозуміти, що саме він перевіряє. Назви файлів завжди закінчуються на Test.php — це конвенція PHPUnit.

Юніт-тестування в Laravel: перевіряємо бізнес-логіку

Юніт-тести — це фундамент пірамід тестування. Вони швидкі, ізольовані і перевіряють конкретну логіку без залежностей. Ось приклад типового юніт-тесту для моделі:

namespace Tests\Unit\Models;

use App\Models\User;
use PHPUnit\Framework\TestCase;

class UserTest extends TestCase
{
    /** @test */
    public function it_can_get_full_name()
    {
        $user = new User([
            'first_name' => 'Олег',
            'last_name' => 'Петренко'
        ]);

        $this->assertEquals('Олег Петренко', $user->full_name);
    }

    /** @test */
    public function it_checks_if_user_is_admin()
    {
        $user = new User(['role' => 'admin']);
        $this->assertTrue($user->isAdmin());

        $regularUser = new User(['role' => 'user']);
        $this->assertFalse($regularUser->isAdmin());
    }
}Code language: PHP (php)

Зверніть увагу на анотацію @test або префікс test_ в назві методу — PHPUnit розпізнає ці методи як тести. Я віддаю перевагу анотаціям, бо назви методів виходять більш читабельними.

Для складної бізнес-логіки створюю окремі сервісні класи і тестую їх:

namespace Tests\Unit\Services;

use Tests\TestCase;
use App\Services\DiscountCalculator;

class DiscountCalculatorTest extends TestCase
{
    private DiscountCalculator $calculator;

    protected function setUp(): void
    {
        parent::setUp();
        $this->calculator = new DiscountCalculator();
    }

    /** @test */
    public function it_calculates_percentage_discount()
    {
        $price = 1000;
        $discount = 15; // 15%
        
        $result = $this->calculator->apply($price, $discount);
        
        $this->assertEquals(850, $result);
    }
}Code language: PHP (php)

Метод setUp() виконується перед кожним тестом — ідеальне місце для ініціалізації об’єктів, які використовуються в кількох тестах.

Тестування в Laravel - приклад виводу PHPUnit тестів у терміналі

Feature-тестування: перевіряємо реальну поведінку

Feature-тести — це те, де Laravel по-справжньому сяє. Ви можете емулювати HTTP-запити, перевіряти редіректи, статус-коди, вміст відповідей — все без запуску браузера. Ось тест для процесу реєстрації:

namespace Tests\Feature\Auth;

use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

class RegisterTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function user_can_register_with_valid_data()
    {
        $response = $this->post('/register', [
            'name' => 'Іван Коваль',
            'email' => 'ivan@example.com',
            'password' => 'SecurePass123',
            'password_confirmation' => 'SecurePass123'
        ]);

        $response->assertRedirect('/dashboard');
        $this->assertDatabaseHas('users', [
            'email' => 'ivan@example.com'
        ]);
    }

    /** @test */
    public function registration_fails_with_invalid_email()
    {
        $response = $this->post('/register', [
            'name' => 'Тест',
            'email' => 'invalid-email',
            'password' => 'password123',
            'password_confirmation' => 'password123'
        ]);

        $response->assertSessionHasErrors('email');
        $this->assertDatabaseCount('users', 0);
    }
}Code language: PHP (php)

Трейт RefreshDatabase — це магія Laravel. Він автоматично створює тестову базу даних перед тестами і відкочує всі зміни після кожного тесту. Ваша реальна база залишається чистою.

Тестування API: JSON та статус-коди

Для API-endpoints Laravel надає спеціальні методи перевірки JSON-відповідей:

/** @test */
public function it_returns_user_list_as_json()
{
    User::factory()->count(3)->create();

    $response = $this->getJson('/api/users');

    $response->assertStatus(200)
             ->assertJsonCount(3, 'data')
             ->assertJsonStructure([
                 'data' => [
                     '*' => ['id', 'name', 'email']
                 ]
             ]);
}Code language: PHP (php)

Метод assertJsonStructure() перевіряє структуру відповіді, не прив’язуючись до конкретних значень — дуже зручно для динамічних даних.

Database Testing: фабрики та сідери

Одна з найпотужніших можливостей тестування в Laravel — це фабрики моделей. Замість ручного створення тестових даних, використовуйте factories:

// database/factories/UserFactory.php
namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    public function definition()
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => bcrypt('password'),
            'remember_token' => Str::random(10),
        ];
    }

    // Можна додавати стани
    public function admin()
    {
        return $this->state(fn (array $attributes) => [
            'role' => 'admin',
        ]);
    }
}Code language: PHP (php)

Тепер у тестах створення даних стає тривіальним:

// Створити одного користувача
$user = User::factory()->create();

// Створити 10 користувачів
$users = User::factory()->count(10)->create();

// Створити адміна
$admin = User::factory()->admin()->create();

// Створити без збереження в БД (для юніт-тестів)
$user = User::factory()->make();Code language: PHP (php)

Переваги використання фабрик:

  • Тести стають короткими та читабельними
  • Легко генерувати багато тестових даних
  • Централізоване місце для структури даних
  • Можливість створювати різні стани моделей

Недоліки та підводні камені:

  • Потрібен час на створення фабрик для кожної моделі
  • Випадкові дані можуть іноді викликати непередбачувану поведінку
  • Складні зв’язки між моделями вимагають ретельного налаштування
  • Важко тестувати edge cases без явного задання значень

Mocking та Dependency Injection у тестуванні Laravel

Коли потрібно протестувати код, що взаємодіє із зовнішніми сервісами (платіжні системи, API, email), використовуйте мокування. Laravel інтегрує Mockery для цього:

use App\Services\PaymentGateway;
use Mockery\MockInterface;

/** @test */
public function it_processes_payment_successfully()
{
    // Мокуємо PaymentGateway
    $this->mock(PaymentGateway::class, function (MockInterface $mock) {
        $mock->shouldReceive('charge')
             ->once()
             ->with(1000, 'user@example.com')
             ->andReturn(['status' => 'success', 'transaction_id' => '12345']);
    });

    $response = $this->post('/checkout', [
        'amount' => 1000,
        'email' => 'user@example.com'
    ]);

    $response->assertStatus(200);
    $this->assertDatabaseHas('transactions', [
        'status' => 'success'
    ]);
}Code language: PHP (php)

Це дозволяє тестувати логіку без реальних викликів платіжних API. Для email Laravel надає фасад Mail з методом fake():

use Illuminate\Support\Facades\Mail;
use App\Mail\OrderShipped;

/** @test */
public function it_sends_order_confirmation_email()
{
    Mail::fake();

    // Виконуємо дію, що має відправити email
    $order = Order::factory()->create();
    $order->ship();

    // Перевіряємо, що email був "відправлений"
    Mail::assertSent(OrderShipped::class, function ($mail) use ($order) {
        return $mail->order->id === $order->id;
    });
}Code language: PHP (php)

Test-Driven Development: практика тестування спочатку

TDD (Test-Driven Development) — це підхід, коли ви пишете тести перед кодом. Звучить дивно, але працює. Цикл простий: спочатку пишете тест (він падає), потім пишете мінімальний код для проходження тесту, потім рефакторите.

Приклад: потрібно створити метод для розрахунку загальної вартості замовлення з податком. Спочатку тест:

/** @test */
public function it_calculates_total_with_tax()
{
    $order = new Order();
    $order->subtotal = 100;
    $order->tax_rate = 0.2; // 20%
    
    $this->assertEquals(120, $order->calculateTotal());
}Code language: PHP (php)

Тест не пройде, бо методу calculateTotal() ще не існує. Тепер створюємо його:

class Order extends Model
{
    public function calculateTotal()
    {
        return $this->subtotal + ($this->subtotal * $this->tax_rate);
    }
}Code language: PHP (php)

Тест проходить! Можна рефакторити, якщо потрібно, але тест залишається зеленим.

Схема циклу Test-Driven Development
Red → Green → Refactor: цикл TDD

Code Coverage: скільки коду покрито тестами

Code coverage показує відсоток коду, який виконується під час тестів. Це корисна метрика, але не абсолютна. 100% покриття не гарантує відсутність багів, але низьке покриття — чіткий сигнал проблеми.

Для генерації звіту code coverage використовуйте Xdebug або PCOV:

php artisan test --coverage

# Або для HTML-звіту
php artisan test --coverage-html=coverage-reportCode language: PHP (php)

У проектах я встановлюю мінімум 70% покриття для критичних модулів (аутентифікація, платежі, бізнес-логіка) і 50% для решти. Це реалістичні цілі, які дають баланс між безпекою та продуктивністю команди.

Continuous Integration: автоматизація тестування

Тести мають запускатися автоматично при кожному push або pull request. Я використовую GitHub Actions для цього:

# .github/workflows/tests.yml
name: Laravel Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.2'
        extensions: mbstring, pdo, pdo_mysql
    
    - name: Install dependencies
      run: composer install
    
    - name: Run tests
      run: php artisan testCode language: PHP (php)

Тепер кожен комміт автоматично перевіряється. Якщо тести не проходять — merge блокується. Це запобігає потраплянню бажаного коду в main гілку.

Кращі практики тестування Laravel: чеклист

Ось практики, які я завжди дотримуюсь у проектах:

  1. Один тест — одна перевірка. Не намагайтесь перевірити все в одному тесті. Краще 5 маленьких тестів, ніж один великий.
  2. Використовуйте описові назви. Назва тесту має пояснювати, що він робить: it_prevents_unauthorized_access_to_admin_panel.
  3. Ізолюйте тести. Кожен тест має бути незалежним і не покладатися на результати інших тестів.
  4. Тестуйте edge cases. Нульові значення, пусті масиви, негативні числа — саме тамховаються баги.
  5. Не тестуйте фреймворк. Laravel вже протестований. Тестуйте вашу бізнес-логіку.
  6. Використовуйте фабрики. Для тестових даних завжди створюйте factories замість ручного заповнення.
  7. Мокуйте зовнішні сервіси. API, email, платежі — все мокується в тестах.
  8. Запускайте тести часто. В ідеалі — після кожної зміни коду.

Плюси комплексного підходу до тестування:

  • Впевненість при рефакторингу — тести швидко покажуть, що зламалось
  • Документація коду — тести показують, як має працювати система
  • Швидше виявлення багів — помилки знаходяться до production
  • Краща архітектура — код, який легко тестувати, зазвичай добре спроектований

Мінуси та виклики:

  • Додатковий час на написання тестів (15-30% від часу розробки)
  • Потреба в підтримці тестів при зміні функціоналу
  • Крива навчання для команди, яка не звикла до тестування
  • Хибне почуття безпеки — тести не замінюють ретельного огляду коду

Інструменти для тестування Laravel

Окрім вбудованих можливостей, є чудові інструменти, які роблять тестування ще зручнішим:

  • Laravel Dusk — для браузерного тестування (E2E тести)
  • Pest PHP — альтернатива PHPUnit з більш елегантним синтаксисом
  • Faker — генерація реалістичних фейкових даних (вбудовано в Laravel)
  • Laravel Telescope — допомагає дебагати тести в процесі розробки

Детальніше про корисні інструменти можна дізнатися в статті про топ-5 інструментів для Laravel-розробника.

Висновки: тестування як інвестиція

Тестування в Laravel — це не витрата часу, а інвестиція. Так, спочатку доведеться витратити більше часу на написання тестів. Але згодом ви заощадите набагато більше на відлагодженні, виправленні багів у production та безсонних ночах після невдалого деплою.

Почніть з малого: напишіть тести для найкритичніших частин вашого додатка. Аутентифікація, платежі, обробка даних користувачів — це мінімум, який має бути покритий тестами. Потім поступово розширюйте покриття.

Пам’ятайте: тести — це не про досягнення 100% покриття. Це про впевненість, що ваш код робить те, що має робити, і продовжує це робити після кожної зміни. А Laravel робить процес тестування настільки зручним, що немає причин цим не користуватися.

Якщо ви тільки розпочинаєте з Laravel, рекомендую спочатку ознайомитись з налаштуванням Laravel на macOS та вибрати зручну IDE для PHP, щоб створити комфортне середовище для розробки та тестування.

Рекомендуємо прочитати

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *