Как применить 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. Также о важности тестов и о том, как их лучше организовать, хорошо описано в книге Роберта Мартина «Чистый код. Создание, анализ и рефакторинг».
Найкращі коментарі пропустити