TDD в PHP: разработка через тесты для реального проекта

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

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

Начать курс

Почему 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 шагов, которые помогут:

  1. Начните с малого. Не пытайтесь покрыть тестами весь legacy-код. Выберите один модуль (например, логику расчёта скидок) и напишите тесты для новых фич.
  2. Пишите тесты до кода. Даже если кажется, что это замедлит — через 2-3 цикла вы заметите, что меньше отлаживаете.
  3. Используйте TDD для багов. Нашли баг? Сначала напишите тест, который его воспроизводит, потом исправляйте. Это гарантирует, что баг не вернётся.
  4. Не гонитесь за 100% покрытием. 80% тестов на критическую логику лучше, чем 100% тестов на геттеры и сеттеры.
  5. Рефакторите без страха. 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: почему это идеальная пара

Когда тесты написаны, их легко интегри

Блоги

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