Введение: Зачем нужно тестирование в 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