Тестирование в PHP: Unit, Integration, E2E от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Зачем нужно тестирование в PHP?

Разработка на PHP без тестирования — это как строить дом без фундамента. Рано или поздно приложение упадёт, а пользователи увидят «белый экран смерти». Тестирование кода — это не роскошь, а необходимость для любого серьёзного проекта. Оно позволяет:

  • Обнаруживать ошибки на ранних стадиях — до того, как код попадёт в продакшн.
  • Упрощать рефакторинг — вы можете смело менять архитектуру, зная, что тесты поймают регрессию.
  • Создавать самодокументируемый код — тесты служат живой документацией того, как должна работать система.
  • Повышать уверенность команды — каждый новый коммит проходит автоматическую проверку.

В PHP существует три основных уровня тестирования: Unit (модульное), Integration (интеграционное) и E2E (сквозное). В этой статье мы разберём каждый из них на практических примерах, от простых assert-ов до полноценных браузерных тестов.

1. Unit-тестирование: тестируем «кирпичики» приложения

Что такое Unit-тест?

Unit-тест проверяет работу одного небольшого блока кода — как правило, одного метода или функции — в полной изоляции от внешних зависимостей (базы данных, API, файловой системы).

Инструменты для PHP

  • PHPUnit — стандарт де-факто. Используется в Laravel, Symfony и многих CMS.
  • Pest — современная альтернатива с более лаконичным синтаксисом, построенная поверх PHPUnit.

Пример: тестируем калькулятор скидок

Предположим, у нас есть простой класс DiscountCalculator:

<?php

class DiscountCalculator
{
    public function calculate(float $price, float $percent): float
    {
        if ($percent < 0 || $percent > 100) {
            throw new \InvalidArgumentException('Процент должен быть от 0 до 100');
        }
        return $price - ($price * $percent / 100);
    }
}

Теперь напишем Unit-тест с помощью PHPUnit:

<?php

use PHPUnit\Framework\TestCase;

class DiscountCalculatorTest extends TestCase
{
    private DiscountCalculator $calculator;

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

    public function testCalculateWithZeroPercent(): void
    {
        $result = $this->calculator->calculate(100, 0);
        $this->assertEquals(100, $result);
    }

    public function testCalculateWithFiftyPercent(): void
    {
        $result = $this->calculator->calculate(200, 50);
        $this->assertEquals(100, $result);
    }

    public function testCalculateThrowsExceptionOnInvalidPercent(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->calculator->calculate(100, 150);
    }
}

Ключевые моменты:

  • Каждый метод тестирует только один сценарий.
  • Мы не обращаемся к БД или внешним сервисам.
  • Используем setUp() для инициализации общих объектов.

Лучшие практики Unit-тестирования

  • Следуйте принципу FIRST: Fast, Isolated, Repeatable, Self-validating, Timely.
  • Именуйте тесты как «test_действие_ожидаемыйРезультат».
  • Избегайте логики в тестах — никаких if/switch/циклов.

2. Интеграционное тестирование: проверяем взаимодействие компонентов

В чём отличие от Unit?

Интеграционные тесты проверяют, как разные части системы работают вместе. Например, как ваш сервис взаимодействует с реальной базой данных, кэшем или внешним API.

Пример: тестируем репозиторий с БД (Laravel + SQLite)

Допустим, у нас есть UserRepository, который сохраняет пользователей в базу данных:

<?php

class UserRepository
{
    public function __construct(private PDO $pdo) {}

    public function save(User $user): int
    {
        $stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
        $stmt->execute([$user->name, $user->email]);
        return (int) $this->pdo->lastInsertId();
    }
}

Интеграционный тест будет использовать тестовую базу данных (например, SQLite in-memory):

<?php

use PHPUnit\Framework\TestCase;

class UserRepositoryTest extends TestCase
{
    private PDO $pdo;
    private UserRepository $repository;

    protected function setUp(): void
    {
        // Создаём in-memory SQLite для изоляции тестов
        $this->pdo = new PDO('sqlite::memory:');
        $this->pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');
        $this->repository = new UserRepository($this->pdo);
    }

    public function testSaveUser(): void
    {
        $user = new User('Иван Петров', 'ivan@example.com');
        $id = $this->repository->save($user);

        $this->assertIsInt($id);
        $this->assertGreaterThan(0, $id);

        // Проверяем, что данные реально записались
        $stmt = $this->pdo->query('SELECT * FROM users WHERE id = ' . $id);
        $savedUser = $stmt->fetch(PDO::FETCH_ASSOC);
        $this->assertEquals('ivan@example.com', $savedUser['email']);
    }

    protected function tearDown(): void
    {
        $this->pdo->exec('DROP TABLE users');
        $this->pdo = null;
    }
}

Важно: В реальных проектах используйте транзакции и откатывайте изменения после каждого теста, чтобы не загрязнять базу данных.

Инструменты для интеграционных тестов

  • PHPUnit + PDO или Doctrine DBAL.
  • L

Блоги

Рекомендаций по книгам