Как применить Test-Driven Development на практике
Всем привет! Меня зовут Денис Оленин, я Tech Lead Back-End Team в компании AmoMedia, которая входит в экосистему бизнесов Genesis. Это моя вторая статья. В первой я рассказывал о «чистом коде» и его базовых принципах на примерах.
Идея статьи о test-driven development (TDD) родилась довольно давно. Мне часто приходится сталкиваться с непониманием, зачем нужны тесты и как их применить в конкретном случае.

Проектов, которые полностью лишены тестов, достаточно много. Любая новая фича может привести к серьезным проблемам в коде, который раньше работал, а QA-команда потратит от пары часов до нескольких дней на полное тестирование проекта. Когда есть модульные тесты и достаточная степень покрытия, такие проблемы практически не возникают.
В первую очередь материал будет полезен новичкам, которые еще не определились с подходом в тестировании своего кода и в целом мало знакомы с тестами.
Краткий ввод в теорию разработки через тестирование
Попробую описать в общих чертах, зачем нам вообще нужны тесты, какие именно тесты стоит создавать в первую очередь, и что же такое «это ваше TDD».
- Говоря просто, тесты помогают быстро найти неисправный код, возникающий после правок или внедрения новых фич. Обычно упавший тест означает проблему в конкретном методе.
- Тесты сильно облегчают рефакторинг.
- Без тестов обновление версий библиотек может стать непосильной задачей.
- Также тесты позволяют создавать более стабильные релизы.
Существует много видов тестирования, но разработчику обычно достаточно покрыть свой код модульными и интеграционными тестами.
Модульными считаются те тесты, которые проверяют минимальные функциональные части кода. Такие части называют атомарными, обычно это простые функции и методы классов. Модульные тесты не должны зависеть от внешних сервисов или других модулей — они работают в изоляции и проверяют поведение только конкретного модуля.
Интеграционные тесты проверяют поведение нескольких методов или функций, которые взаимодействуют друг с другом.
Суть TDD-подхода — написать все необходимые тесты до того, как мы приступим к созданию кода. Для многих это прозвучит странно: как можно писать тесты для пустого места?
Создавая тесты до кода, мы углубляемся в тематику проекта со стороны контракта (интерфейса) и, таким образом, лучше понимаем его итоговый вид. А это значит, что уже при разработке бизнес-логики нам придется тратить меньше времени на декомпозицию и переписывание одних и тех же участков кода из-за недопроектирования.
TDD также применим в проектах, где нет тестов, но кода уже написали много. В этом случае подход используется для новых фич. Внедрить тесты, как и сам TDD, никогда не поздно.
Разработка новых фич по TDD
Некоторые разработчики описывают TDD-подход, как существующий исключительно в теории и совершенно неприменимый в реальности. На самом деле, он не только хорошо себя показывает на практике, но и привносит дополнительные плюшки. В этом разделе речь пойдет о том, как применить TDD.
Представим, что нам достался новый проект и мы все-таки решили внедрить такой подход.
С чего начать
- Сперва декомпозируем техническое задание на фичи и заводим их в любимый баг-трекер в виде тасок.
- Также нужно правильно распределить приоритеты тасок: не делаем фичу Б, если она не может работать без фичи А — сначала делаем фичу А.
- Далее пишем модульные тесты на первую фичу и проверяем, проходят ли они. Это своего рода тестирование самих тестов. Желательно написать сразу позитивный и негативный тест под фичу.
- Приступаем к написанию кода бизнес-логики. Код фичи готов, когда все тесты пройдены.
- Рефакторим код, если это нужно.
- Повторяем цикл с новой фичей.
Визуально этот процесс можно показать так:

Это достаточно простой подход, но иногда возникает ряд сложностей при написании самих тестов:
- тестирование приватных методов;
- тестирование методов, взаимодействующих с БД;
- тестирование методов, взаимодействующих со сторонними сервисами;
- предварительно подготовленные данные.
Тестирование приватных методов
Чаще всего их просто игнорируют. В большом проекте может быть много приватных методов, их так же важно протестировать. Для этого чаще всего рекомендуют использовать рефлексию:
public function invokeMethod(&$object, $methodName, array $parameters = array())
{
$reflection = new \ReflectionClass(get_class($object));
$method = $reflection->getMethod($methodName);
$method->setAccessible(true);
return $method->invokeArgs($object, $parameters);
}
Это рабочий подход, но интерфейс рефлексии не самый удобный. Поэтому я советую использовать Closure. Этот подход позволяет привязать контекст замыкания к тестируемому классу в рантайме. Выглядит так, будто замыкание выполняется как собственный метод класса.
Благодаря Closure можно получить доступ ко всем свойствам и методам класса. Такой подход имеет более понятный интерфейс.
class SomeClass {
private function somePrivateMethod(): string {
return "somePrivateData";
}
}
$someObject = new SomeClass;
$someValue = Closure::bind(function() {
return $this->somePrivateMethod();
},
$someObject,
$someObject
)($someObject);
Либо начиная с PHP версии 7.4 можно этот же подход реализовать через стрелочную функцию:
$someValue = ((fn() => $this->somePrivateMethod())->bindTo($someObject, $someObject))();
Тестирование методов, взаимодействующих со сторонними сервисами
Для таких методов стоит создавать mock-объекты. Это означает, что вам нужно сделать поддельную версию внешнего или внутреннего сервиса, который позволит вашим тестам работать в изоляции от таких зависимостей.
Когда ваша реализация взаимодействует со свойствами объекта, а не с его методом или поведением, можно использовать mock.
$mock = $this->createMock(PushTransport::class);
$mock->expects($this->once())->method('send');
В этом случае реальной отправки сообщения не будет, но будет вызван метод созданной нами заглушки. При этом мы можем проверить, сколько раз он будет вызван и будет ли вызван вообще.
Тестирование методов, взаимодействующих с БД
Если у вас есть некий метод, который должен что-то прочитать или записать в БД, и вам нужно написать модульный тест для этого метода, то не стоит разворачивать для этого отдельный экземпляр сервера БД. Обычно достаточно настроить для тестового окружения подключение к sqlite.
Это достаточно легковесная БД. Если вы не используете в приложении синтаксис, который sqlite не поддерживает, то работать с БД в тестовом окружении станет проще. Иногда вместо sqlite можно использовать mock-объекты как в предыдущем случае.
Предварительно подготовленные данные
Также стоит заранее создать все необходимые данные для тестов (seeds или fixtures). Это классы с фабриками, которые помогают генерировать фейковые данные для тестовых случаев или данные которые должны быть в системе заранее (например, таким образом можно создать запись root пользователя в системе).
final class AudienceFixtures extends Fixture
{
use WithFaker;
public function load(ObjectManager $manager)
{
$faker = $this->getFaker();
$project = new Project;
$project->setName($faker->name);
$project->setHost($faker->domainName);
$manager->persist($project);
$audience = new Audience;
$audience->setName($faker->name);
$audience->setSubscribedAfter((new DateTime));
$audience->addProject($project);
$manager->persist($audience);
$manager->flush();
}
}
Почему стоит внедрить TDD на старте проекта
Существуют стереотипы, что тесты занимают много времени или что тестами достаточно покрыть только жизненно важный функционал. Но важным обычно оказывается практически весь функционал, и любой, казалось бы, второстепенный метод может уронить систему.
В краткосрочной перспективе написание кода с тестами и вправду занимает немного больше времени, но полученный код будет стабильнее и разработчику нужно будет реже возвращаться к нему, чтобы фиксить баги.
Это ошибка — думать, что тесты существуют отдельно от кода, бизнес-логики и не страшно, если сделать их позже. Обычно это время никогда не наступает.
Для того чтобы лучше понять проект, стоит всегда начинать с покрытия тестами. Если вы еще их не пишите, то рекомендую потратить время на изучение темы и все же начать это делать. Лучше сразу попробовать писать по методологии TDD.
Список полезных ресурсов
1. Вебинар на тему тестирования в целом и TDD. В нем хорошо объясняется, как начать писать тесты, работать с TDD, какие тесты бывают и как создавать код так, чтобы его можно было тестировать.
2. Книга Кена Бэка «Экстремальное программирование: разработка через тестирование». Автор рассматривает применение TDD на примере разработки реального программного кода и тем самым демонстрирует простоту и мощь этой методики.
3. Также о важности тестов и о том, как их лучше организовать, хорошо описано в книге Роберта Мартина «Чистый код. Создание, анализ и рефакторинг».
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.
Найкращі коментарі пропустити