PHP Unit Testing: практичний гайд для початківців

Поговоримо в цій статті про PHP Unit Testing. Коли я тільки починав писати PHP-код, тестування здавалося чимось надто складним і зайвим. Навіщо витрачати час на написання тестів, якщо можна просто перевірити функціонал у браузері? Але після першого серйозного багу на продакшені, який міг би виявити звичайний юніт-тест, я зрозумів — автоматизоване тестування не розкіш, а необхідність.

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

Що таке PHP Unit Testing (юніт-тестування) і навіщо воно потрібне

Юніт-тестування — це процес перевірки окремих компонентів вашого коду (функцій, методів, класів) на правильність роботи. Уявіть, що ви будуєте будинок: перед тим як монтувати всю конструкцію, ви перевіряєте кожну цеглину окремо. Так само працюють юніт-тести — вони гарантують, що кожен маленький шматочок вашого додатку працює так, як очікується.

Основні переваги юніт-тестування:

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

Але є й мінуси, про які чесно треба сказати:

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

Встановлення PHPUnit

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

Відкрийте термінал у кореневій директорії вашого проєкту і виконайте:

composer require --dev phpunit/phpunitCode language: JavaScript (javascript)

Прапорець --dev каже Composer, що PHPUnit потрібен тільки для розробки, а не для продакшену. Після встановлення перевірте версію:

./vendor/bin/phpunit --version

Якщо побачили щось на кшталт “PHPUnit 10.x.x”, вітаю — все працює! Тепер створіть файл конфігурації phpunit.xml у корені проєкту. Це необов’язково, але дуже зручно:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
         colors="true"
         verbose="true">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>./tests</directory>
        </testsuite>
    </testsuites>
</phpunit>Code language: HTML, XML (xml)

Цей конфіг каже PHPUnit шукати тести в папці tests і підключати автолоадер Composer.

Перший тест: калькулятор простих чисел

Теорія — це добре, але давайте нарешті напишемо щось реальне. Припустимо, у нас є простий клас калькулятора:

<?php
// src/Calculator.php

namespace App;

class Calculator
{
    public function add(int $a, int $b): int
    {
        return $a + $b;
    }

    public function divide(float $a, float $b): float
    {
        if ($b === 0.0) {
            throw new \InvalidArgumentException('Ділення на нуль неможливе');
        }
        return $a / $b;
    }
}Code language: HTML, XML (xml)

Тепер створимо тест для цього класу. В PHPUnit файли тестів мають закінчуватися на Test.php, а класи — наслідувати PHPUnit\Framework\TestCase:

<?php
// tests/CalculatorTest.php

namespace Tests;

use App\Calculator;
use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    private Calculator $calculator;

    // Цей метод виконується перед кожним тестом
    protected function setUp(): void
    {
        $this->calculator = new Calculator();
    }

    public function testAddReturnsCorrectSum(): void
    {
        // Arrange (підготовка)
        $a = 5;
        $b = 3;

        // Act (дія)
        $result = $this->calculator->add($a, $b);

        // Assert (перевірка)
        $this->assertEquals(8, $result);
    }

    public function testDivideReturnsCorrectResult(): void
    {
        $result = $this->calculator->divide(10, 2);
        $this->assertEquals(5, $result);
    }

    public function testDivideByZeroThrowsException(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->calculator->divide(10, 0);
    }
}Code language: HTML, XML (xml)

Звернули увагу на структуру Arrange-Act-Assert? Це класичний патерн написання тестів: спочатку готуємо дані, потім виконуємо дію, і нарешті перевіряємо результат. Запустіть тести:

./vendor/bin/phpunit

Якщо побачили зелені галочки — все працює ідеально!

PHPUnit Pretty Result (laravel news)

Assertions: мова тестів PHPUnit

Assertions (твердження) — це серце юніт-тестів. Вони перевіряють, чи відповідає реальний результат очікуваному. PHPUnit пропонує десятки різних assertions, але ось найкорисніші для початку:

  • assertEquals($expected, $actual) — перевіряє рівність значень
  • assertSame($expected, $actual) — перевіряє ідентичність (включно з типом)
  • assertTrue($condition) та assertFalse($condition) — булеві перевірки
  • assertNull($value) та assertNotNull($value) — перевірка на null
  • assertEmpty($value) — перевіряє, що значення порожнє
  • assertCount($expectedCount, $array) — перевіряє розмір масиву
  • assertStringContainsString($needle, $haystack) — перевіряє наявність підрядка

Приклад практичного використання:

public function testUserDataValidation(): void
{
    $user = [
        'name' => 'John Doe',
        'email' => 'john@example.com',
        'age' => 25
    ];

    $this->assertIsArray($user);
    $this->assertCount(3, $user);
    $this->assertArrayHasKey('email', $user);
    $this->assertStringContainsString('@', $user['email']);
    $this->assertGreaterThan(18, $user['age']);
}Code language: PHP (php)

Data Providers: тестуємо багато сценаріїв одночасно

Часто потрібно протестувати одну функцію з різними вхідними даними. Замість копіювання коду можна використовувати Data Providers — це геніальна фіча PHPUnit:

class CalculatorTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAddWithMultipleInputs(int $a, int $b, int $expected): void
    {
        $calculator = new Calculator();
        $this->assertEquals($expected, $calculator->add($a, $b));
    }

    public static function additionProvider(): array
    {
        return [
            'positive numbers' => [2, 3, 5],
            'negative numbers' => [-5, -3, -8],
            'mixed numbers' => [-10, 15, 5],
            'with zero' => [0, 7, 7],
        ];
    }
}Code language: PHP (php)

Тепер один тест запуститься чотири рази з різними даними. Якщо якийсь сценарій провалиться, ви одразу побачите який саме — завдяки іменованим ключам масиву.

Mocking: тестуємо складні залежності

Реальні додатки рідко існують у вакуумі — вони взаємодіють з базами даних, API, файловою системою. Але під час юніт-тестування ми не хочемо чіпати реальну базу чи слати справжні HTTP-запити. Тут на допомогу приходять моки (mocks) — підробки об’єктів.

Припустимо, у нас є сервіс, що відправляє email:

<?php

namespace App;

class UserService
{
    public function __construct(
        private MailerInterface $mailer
    ) {}

    public function registerUser(string $email): bool
    {
        // Якась логіка реєстрації...
        
        $this->mailer->send($email, 'Ласкаво просимо!');
        return true;
    }
}Code language: HTML, XML (xml)

Тест із моком виглядатиме так:

public function testRegisterUserSendsEmail(): void
{
    // Створюємо мок об'єкта mailer
    $mailerMock = $this->createMock(MailerInterface::class);
    
    // Очікуємо, що метод send буде викликаний рівно один раз
    $mailerMock->expects($this->once())
        ->method('send')
        ->with(
            $this->equalTo('test@example.com'),
            $this->stringContains('Ласкаво')
        );

    $userService = new UserService($mailerMock);
    $result = $userService->registerUser('test@example.com');
    
    $this->assertTrue($result);
}Code language: PHP (php)

Моки дозволяють перевіряти не тільки результат, а й те, як ваш код взаємодіє з іншими компонентами. Це надзвичайно корисно для тестування бізнес-логіки.

Поширені помилки початківців

За роки роботи я бачив безліч типових помилок у тестах. Ось топ-5, які варто уникати:

1. Тестування чужого коду

Не потрібно тестувати вбудовані PHP-функції або код сторонніх бібліотек. Вони вже протестовані:

// Погано - тестуємо вбудовану функцію PHP
public function testArrayMerge(): void
{
    $result = array_merge([1, 2], [3, 4]);
    $this->assertEquals([1, 2, 3, 4], $result);
}Code language: PHP (php)

2. Занадто великі тести

Один тест = одна перевірка. Якщо тест перевіряє багато речей, його важко підтримувати:

// Погано - занадто багато assertions в одному тесті
public function testUserCreation(): void
{
    $user = new User('John', 'john@test.com');
    $this->assertEquals('John', $user->getName());
    $this->assertNotEmpty($user->getId());
    $this->assertTrue($user->isActive());
    $this->assertInstanceOf(DateTime::class, $user->getCreatedAt());
    // ... ще 10 перевірок
}Code language: PHP (php)

3. Залежності між тестами

Кожен тест має бути незалежним. Ніколи не покладайтеся на порядок виконання тестів або на стан після попереднього тесту.

Інтеграція з популярними фреймворками

Якщо ви працюєте з Laravel чи іншим фреймворком, там зазвичай вже є готова інтеграція з PHPUnit. У Laravel, наприклад, можна тестувати HTTP-запити надзвичайно просто:

public function testHomePage(): void
{
    $response = $this->get('/');
    
    $response->assertStatus(200);
    $response->assertSee('Ласкаво просимо');
}Code language: PHP (php)

Детальніше про тестування в Laravel читайте в нашому гайді по Laravel Eloquent.

Корисні інструменти та практики

Щоб зробити роботу з тестами ще зручнішою, рекомендую:

  • Code Coverage — перевіряйте, яку частину коду покривають тести. Команда: ./vendor/bin/phpunit --coverage-html coverage
  • PHPStorm інтеграція — запускайте тести прямо з IDE, клікаючи на зелену стрілку біля методу
  • Git hooks — налаштуйте автоматичний запуск тестів перед кожним комітом
  • CI/CD — інтегруйте PHPUnit у ваш pipeline (GitHub Actions, GitLab CI тощо)

Для налаштування CI/CD процесів рекомендую ознайомитись з GitHub Actions для Laravel.

Висновок

Юніт-тестування — це не магія і не ракетна наука. Це просто навичка, яку можна вивчити за кілька днів практики. Почніть з малого: напишіть тести для однієї простої функції. Потім для класу. Потім для модуля. Дуже швидко ви відчуєте, як тести дають впевненість у вашому коді.

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

Офіційна документація PHPUnit доступна на phpunit.de, а детальніше про можливості фреймворку можна дізнатись у документації PHPUnit.


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

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

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