Почему TDD стал стандартом для PHP-разработчиков?
Представьте: вы пишете код, запускаете его, видите ошибку, исправляете — и так по кругу. Знакомо? А теперь представьте, что вы сначала пишете тест, который заведомо упадёт, потом минимальный код, чтобы он прошёл, и только потом рефакторите. Это и есть TDD в PHP — разработка через тестирование, которая превращает хаос в стройную систему.
Многие разработчики боятся TDD: «Это же замедлит разработку!», «Я не знаю, как тестировать legacy-код», «PHPUnit — это сложно». Но на практике TDD экономит часы отладки и делает код устойчивым к изменениям. В этой статье мы разберём TDD на реальных примерах PHP, покажем, как работает цикл «красный-зелёный-рефакторинг», и дадим готовые шаблоны для внедрения.
Что такое TDD и как это работает в PHP?
Разработка через тестирование (Test-Driven Development) — это методология, где тесты пишутся до реализации. Основной цикл выглядит так:
- Красный — пишем тест, который не проходит (ожидаем ошибку).
- Зелёный — пишем минимальный код, чтобы тест прошёл.
- Рефакторинг — улучшаем код, не меняя поведение.
В PHP для TDD чаще всего используют PHPUnit — мощный фреймворк для юнит-тестирования. Но TDD — это не про инструменты, а про дисциплину. Давайте посмотрим, как это работает на практике.
Пример 1: Тестируем простой калькулятор
Допустим, нам нужна функция сложения двух чисел. Без TDD мы бы написали:
function add($a, $b) {
return $a + $b;
}
echo add(2, 3); // 5
С TDD мы сначала пишем тест:
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase {
public function testAdd() {
$calculator = new Calculator();
$result = $calculator->add(2, 3);
$this->assertEquals(5, $result);
}
}
Запускаем — тест падает (красный). Пишем минимальную реализацию:
class Calculator {
public function add($a, $b) {
return $a + $b;
}
}
Тест проходит (зелёный). Рефакторинг не нужен — код прост. Но если бы мы захотели добавить обработку отрицательных чисел, сначала написали бы тест для этого случая.
Пример 2: TDD для работы с базой данных
Часто говорят: «TDD не подходит для БД». Это миф. Используйте моки (mocks) и тестовые базы данных. Рассмотрим пример с UserRepository:
class UserRepositoryTest extends TestCase {
public function testFindUserById() {
$pdoMock = $this->createMock(PDO::class);
$stmtMock = $this->createMock(PDOStatement::class);
$pdoMock->method('prepare')
->willReturn($stmtMock);
$stmtMock->method('fetch')
->willReturn(['id' => 1, 'name' => 'Alex']);
$repository = new UserRepository($pdoMock);
$user = $repository->findById(1);
$this->assertEquals('Alex', $user['name']);
}
}
Тест проверяет, что репозиторий правильно вызывает SQL и обрабатывает результат. Код реализации будет минимальным: просто выполнить запрос и вернуть данные.
Популярные инструменты для TDD в PHP
Чтобы TDD стал частью вашего workflow, нужно освоить несколько инструментов:
- PHPUnit — основной фреймворк для тестов. Установка:
composer require --dev phpunit/phpunit. - Mockery — библиотека для создания моков с гибким API.
- Prophecy — альтернатива Mockery, часто используется с Symfony.
- PHPStan / Psalm — статический анализ, который дополняет TDD, находя ошибки до запуска тестов.
Для интеграционных тестов можно использовать Laravel Dusk или Symfony Panther, но для начала достаточно PHPUnit.
Практические советы: как внедрить TDD в PHP-проект
Внедрение TDD — это не просто «начать писать тесты». Вот 5 шагов, которые помогут:
- Начните с малого. Не пытайтесь покрыть тестами весь legacy-код. Выберите один модуль (например, логику расчёта скидок) и напишите тесты для новых фич.
- Пишите тесты до кода. Даже если кажется, что это замедлит — через 2-3 цикла вы заметите, что меньше отлаживаете.
- Используйте TDD для багов. Нашли баг? Сначала напишите тест, который его воспроизводит, потом исправляйте. Это гарантирует, что баг не вернётся.
- Не гонитесь за 100% покрытием. 80% тестов на критическую логику лучше, чем 100% тестов на геттеры и сеттеры.
- Рефакторите без страха. TDD даёт вам сетку безопасности: если после рефакторинга тесты зелёные — всё ок.
Типичные ошибки новичков в TDD
- Писать тесты после кода (это уже не TDD, а просто тестирование).
- Тестировать слишком много в одном тесте (один тест — одна проверка).
- Использовать реальные базы данных в юнит-тестах (это замедляет и делает тесты хрупкими).
- Игнорировать краевые случаи (пустые массивы, null, отрицательные числа).
Реальный кейс: TDD для формы регистрации
Допустим, мы делаем форму регистрации. Валидация должна проверять email и пароль. Начнём с теста:
class RegistrationValidatorTest extends TestCase {
public function testValidEmail() {
$validator = new RegistrationValidator();
$this->assertTrue($validator->isEmailValid('test@example.com'));
}
public function testInvalidEmail() {
$validator = new RegistrationValidator();
$this->assertFalse($validator->isEmailValid('invalid-email'));
}
public function testPasswordMinLength() {
$validator = new RegistrationValidator();
$this->assertFalse($validator->isPasswordValid('12345')); // меньше 6 символов
$this->assertTrue($validator->isPasswordValid('123456'));
}
}
Пишем минимальную реализацию:
class RegistrationValidator {
public function isEmailValid(string $email): bool {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
public function isPasswordValid(string $password): bool {
return strlen($password) >= 6;
}
}
Тесты проходят. Теперь можно рефакторить: например, добавить проверку на сложность пароля. Но сначала — новый тест!
TDD и CI/CD: почему это идеальная пара
Когда тесты написаны, их легко интегри