Где TDD окупается, а где нет
Что это такое
Форма цикла «красный — зелёный — рефакторинг» подходит одним задачам лучше, чем другим, и потому это инструмент, а не универсальное правило. Лучше всего он окупается там, где поведение укладывается в правило с входами и выходами и где вы уже примерно представляете, что именно строите. Хуже всего — на этапе исследования: проектирование ещё ищут, а интерфейс меняется каждые несколько минут. Умение состоит в том, чтобы понимать, чем именно вы заняты сейчас.
Почему это важно
Спорят сторонники и скептики обычно мимо друг друга: каждый представляет себе свою работу. Тот, кто строит движок расчёта цен, считает цикл естественным и не понимает, зачем ему сопротивляться; тот, кто прототипирует интерфейс, видит в нём смирительную рубашку и не понимает, зачем вообще возиться. Насчёт собственной задачи правы оба. Однако навязанный цикл там, где он мешает, даёт догматичный код и заброшенные наборы тестов, а пропущенный там, где он уместен, — непокрытую ветку, которая уносит ошибку в продакшен.
Ключевые идеи
Он окупается для логики с правилами
Идеальны бизнес-правила, расчёты, парсеры, конечные автоматы и алгоритмы: поведение описывается случаями, каждый случай становится тестом, а реализация прячется за стабильной границей. Сильнее всего это заметно на исправлении ошибок — напишите тест, воспроизводящий дефект, посмотрите, как он падает, и только потом правьте код. Такой тест остаётся единственным доказательством, что ошибка исчезла, и единственной защитой от её возвращения.
отчёт об ошибке -> тест, её воспроизводящий (красный) -> исправление (зелёный) -> больше не ломается
Он окупается там, где проектирование уже видно
Если вы знаете, как называется модуль и что он принимает, тест, написанный первым, не стоит ничего, а интерфейс вы получаете даром. Именно поэтому со второй и третьей функцией в модуле TDD идёт легко, а с первой — неуклюже. Первая функция и есть работа над проектированием, а она по сути исследование.
Он борется с вами во время исследования
Спайки, прототипы, компоновка интерфейса и вообще всё, что вы собираетесь выбросить, не стоит писать через тесты. Ценность спайка — в скорости, а будущего у такого кода нет. Дисциплина требует другого: признать спайк, а потом удалить его, и не дать одноразовому коду без тестов стать продуктом — именно так и рождается большая часть непокрытого кода.
спайк -> узнать -> удалить спайк -> написать настоящее, начиная с теста
Он неудобен для связующего кода и конфигурации
У кода, который в основном связывает чужие части между собой — настройка внедрения зависимостей, таблицы маршрутов, разбор конфигурации, — почти нет собственного поведения, которое стоило бы утверждать. Правильность такого кода доказывает интеграционный тест, который запускает систему, а модульные тесты дают лишь хрупкие утверждения о порядке вызовов и почти ничего не покупают.
Тест раньше против теста после
Тесты после кода — не преступление, а просто другое занятие. Защиту от регрессий они дают ничуть не хуже, и в этом большая часть их ценности. Зато теряется давление на проектирование — тот момент, когда модуль, который трудно тестировать, говорит, что интерфейс неверен, — и гарантия, что тест вообще способен провалиться. Если пишете тесты после, хотя бы раз сломайте код и посмотрите, как они упадут.
Практика
Напишите тест раньше для ошибки
Возьмите дефект, который вы исправили недавно, или тот, о котором знаете, но ещё не исправили. Напишите тест, который его воспроизводит и падает, а затем почините код.
Готово, когда: тест падает до исправления и проходит после, а его название называет дефект.
Объявите намеренный спайк
Выберите очередную исследовательскую задачу из своего списка и заранее объявите её спайком: срок, никаких тестов, код после работы выбрасывается.
Готово, когда: спайк удалён, а сохранённая часть переписана через тесты быстрее, чем вы ожидали.
Частые ошибки
- Цикл, навязанный спайку — исследование замедляется до черепашьего шага, а код всё равно идёт в мусор; исправить: объявите спайк, ограничьте его срок, удалите его.
- Пропуск цикла для «очевидной» логики — крайний случай, о котором никто не подумал, уезжает в продакшен; исправить: применяйте цикл к правилам и расчётам и пишите тест, который падает, на каждую ошибку.
- Модульные тесты для конфигурационной связки — они утверждают порядок вызовов и ломаются при каждом рефакторинге; исправить: покройте связку интеграционным тестом, который запускает систему.
- Разговор про TDD, когда тесты пишут последними — обратная связь по проектированию потеряна, и ни один тест ни разу не падал; исправить: либо пишите тесты первыми, либо честно называйте это тестами после.
- Покрытие как цель — высокое покрытие с пустыми утверждениями ничего не доказывает; исправить: смотрите, падают ли тесты, когда код ломается.
Что читать дальше
- Цикл «красный — зелёный — рефакторинг» (01-cs-fundamentals/20-test-driven-development-tdd/01-the-red-green-refactor-loop) — механика цикла, поэтому читайте первым.
- Модульное тестирование (01-cs-fundamentals/10-unit-testing) — как писать утверждения, которые в самом деле способны провалиться.
- Интеграционное тестирование (01-cs-fundamentals/21-integration-testing) — верный уровень для связки и обвязки, с которыми модульные тесты справляются плохо.