Test Driven Development
(TDD) это один из очень уважаемых подходов к разработке и считается признаком высокой культуры подхода к разработке.
Кроме того это одна из первых “больших” идей в программировании, с которой я познакомился через книгу
Гарри Персиваля “Python. Разработка на основе тестирования”. Но недавно для себя я решил пока полностью
отказаться от этой идеи, по крайней мере в собственных проектах, и вот почему.
Для меня в большинстве своём TDD приводит к тому, что приходится выполнять двойную работу - каждое изменение вызывает необходимость изменять сигнатуру функций в тестах, названия, импорты (хорошо, если LSP может сделать такой рефакторинг за нас), добавить новые моки и т.д., затем вернуться, чтобы попробовать новую идею. Каждый раз когда ты осознаёшь, что пару классов можно слить в один, другой класс напротив разделить на несколько, что стоит перераспределить методы, сделать модуль чуть меньше - сначала тебя ждёт эта экстра-работа по переделке всех этих уже прописанных тестов. Это просто создаёт слишком много трения в работе - слишком большой зазор между идеей и тем как возникает её первый работающий прототип. И этот момент очень важен, так как маленький заряд дофамина от того, что “оно хотя бы как то едет” это и есть радость от программирования, такие моменты в течение дня складываются в удовлетворение от работы. Но с TDD надо мной постоянный “груз” дополнительной работы, которой я расплачиваюсь за каждую новую идею и каждое отклонение от первоначальной задумки.
Этот дополнительный груз в значительной степени мешает креативности - свобода пробовать идеи и легко отбрасывать их. Поиск решения зачастую это целое исследование, с множеством развилок и тупиков. TDD же способствует тому чтобы мы придерживались всегда изначального плана, иначе нас ждет дополнительная работа. Мне же гораздо ближе что-то вроде техники бриколажа - прилаживание, быстрая обратная связь и коррекция.
Не поймите меня неправильно - тесты нужны. Как мне кажется, не столько с точки зрения даже того, чтобы убедиться, что все работает сейчас. Во многом чтобы не ощущать себя “бросающим камни в стеклянном доме” при внесении изменений. Если я что-то сломал, то должен знать об этом максимально быстро, и регрессионное тестирование здесь это просто лучший инструмент, чтобы избежать такой ситуации.
TDD очень хорошо решает следующую проблему - в не-TDD случае после завершения любой фичи мы оказываемся в следующей ситуации:
- Заряд мотивации высвободится и теперь ушел.
- Впереди вероятно ещё куча работы, которая более интересная чем писать тесты, а может более срочная и нужная с точки зрения руководства. На самом деле не так уж просто доказать кому-то, что если что-то уже работает сейчас, то мне нужно потратить ещё возможно столько же времени на то чтобы просто убедиться что так будет всегда.
В среднем вероятность что тесты будут написаны в такой ситуации очень нестабильна и зависит от уровня собственной усталости, дисциплинированности, культуры в команде по отношению к тестам и т.д.
TDD ловко решает эту проблему, просто поставив телегу впереди лошади (а именно так TDD, честно говоря, и ощущается:)). Теперь мы никуда не поедем пока тесты не будут написаны и отработаны, получаем гарантию что они вообще будут существовать.
Есть ли какие-то альтернативы, чтобы повысить наши шансы на то, что тесты будут написаны? Тут есть несколько идей:
Defensive programming
Одна из идей - это делить свои сессии программирования на “креативные” и “защитные”.
Во время вторых мы должны попытаться “атаковать” наш уже работающий и хоть немного устоявшийся код
с позиций валидации инпутов, потенциальных исключений, краевых случаев и так далее. В идеале каждая
выявленная уязвимость влечёт за собой создание теста или хотя бы assert
.
Не обязательно эти сессии должны чётко чередоваться поочерёдно, но важно то, что этапы творческих попыток,
экспериментов и создание работающего прототипа всегда предшествуют “критическому” периоду.
Главное убедить себя что сессии defensive programming подходов по важности на одинаковом уровне с креативной разработкой.
Assert
- представляет собой юнит-тест в миниатюре. Он отражает какое-то наше предположение о работе когда и
немедленно останавливает его работу с указанием на место, если это предположение неверно. Большой его плюс
перед обычными тестами в том, что он встроен в код и его можно в любой момент добавить или убрать “походу”
разработки без потери фокуса.
Приоритет E2E тестов.
Мы не пишем модульные тесты до тех пор, пока не будут созданы E2E тесты. Такие тесты являют собой точку зрения конечного пользователя на систему, то как её видит непосредственно он. Модульные тесты добавляются только если симулировать краевые случаи как-то совсем уж сложно. Конечно, такой подход не гарантирует что эти E2E будут написаны (тут мы немного перекладываем ответственность в другое место), но с человеческой точки зрения они кажутся гораздо более важными и осмысленными - так как буквально производят “пользование” конечным продуктом, а значит имеют больше мотивации для написания.
Случаи когда применение TDD кажется уместным:
- TDD уже используется в команде и это общепринятый и обговорённых подход. В этом случае остаётся только следовать за принятыми установками. Но вероятно это будет проще потому что возможно будет существовать разделение внутри между непосредственно вдумчивой подготовкой тестов и реализацией кода на их основе.
- Мы абсолютно точно знаем как будет выглядеть юнит кода, который собираемся написать. Самый яркий пример здесь это конечно условные задачи на Литкоде, у которых чётко прописаны входные/выходные данные и уже задана сигнатура функции/класса. Другой пример - если расширяется уже существующий функционал (ещё один подкласс, еще одна реализация существующего алгоритма, ещё одна схожая модель в Django и т.д.), в таком случае мы не изобретаем новое, но расширяем программу, вставляя новое в уже существующие “пазы”. Когда не нужно думать про программу как целое и можно на 100% сосредоточится на её части.