TDD — Test Driven Development

Движущая сила: тесты как исполняемая спецификация — именно они направляют дизайн и реализацию.

Уровень применения: код и команда (практики инженеров; поддерживается CI и процессом).

Статус: устоявшийся (сформирован в конце 1990-х, входит в ядро Agile/XP-практик).

Не путать с: Type Driven Development (та же аббревиатура TDD, но движущая сила — типы, а не тесты; «зелёный» означает «компилируется», а не «тесты проходят»); BDD и ATDD — о проверке поведения и критериев приёмки на уровне требований.

Общее

Test Driven Development (TDD) — методология разработки, в которой для каждой единицы функциональности сначала пишется автоматический тест (описывающий ожидаемое поведение), и лишь затем — код, делающий этот тест зелёным. Тест здесь не артефакт постфактум, а первичный драйвер дизайна: формулируя проверку до реализации, разработчик вынужден думать о том, как новый код будет вызываться, а значит — о его интерфейсе, связанности и тестируемости.

Метод был сформулирован Кентом Беком (Kent Beck) в конце 1990-х в рамках методологии Extreme Programming (XP) и подробно описан в книге Test-Driven Development: By Example (2002). Бек неоднократно подчёркивал, что придумал не «написание тестов» (они существовали и раньше), а короткий дисциплинированный цикл, в котором тест управляет процессом написания кода.

Сердце TDD — цикл из трёх фаз, известный как Red → Green → Refactor:

  1. Red. Пишется тест под ещё не существующую функциональность. Тест запускается и обязательно падает — причём падает по правильной причине (не компилируется / нет метода / неверный результат), а не из-за ошибки в самом тесте. Красный цвет сигнализирует: спецификация зафиксирована, теперь нужен код.
  2. Green. Пишется минимально достаточный production-код, чтобы тест прошёл. Цель фазы — получить зелёный свет как можно быстрее, а не написать «правильную» архитектуру. Допускаются нарочито примитивные решения, вплоть до возврата константы («fake it»).
  3. Refactor. Production-код и тесты улучшаются при сохранении зелёного состояния. Дублирование убирается, имена уточняются, выделяются абстракции — но без изменения поведения. Это фаза, ради которой, по сути, и существует цикл: именно здесь рождается чистый дизайн, и именно здесь безопасно, потому что тесты страхуют.

Эти три фазы повторяются десятки и сотни раз за рабочий день, с шагом в минуты. Бек сформулировал это как «три закона TDD» (популяризированы Робертом Мартином):

  1. Не пишите production-код, пока не написан тест, который падает из-за его отсутствия.
  2. Не пишите теста больше, чем достаточно, чтобы он падал (не компилировался — уже достаточный «падение»).
  3. Не пишите production-кода больше, чем достаточно, чтобы пройти текущий падающий тест.

Из этих трёх законов с необходимостью вытекает весь остальной стиль: маленькие шаги, постоянный зелёный цвет, частый рефакторинг, тесты как спецификация.

TDD — зонтичное понятие, и под ним в индустрии понимают несколько связанных, но не тождественных практик:

  • TDD в строгом смысле (Test-First / Micro-TDD) — цикл Red-Green-Refactor на уровне единиц (юнит-тесты). Именно об этом статья.
  • ATDD (Acceptance Test Driven Development) — тесты пишутся на уровне критериев приёмки пользователе-ориентированной функциональности; драйвер — не разработчик в одиночку, а вся команда с заказчиком.
  • BDD (Behaviour Driven Development) — расширение ATDD с акцентом на общий язык (ubiquitous language) и сценарии «Given-When-Then»; фокус смещён с тестирования на взаимодействие бизнеса и разработки через поведение.

Эти три подхода родственны, но живут на разных уровнях: TDD — это инженерная микро-практика, ATDD/BDD — практики командного взаимодействия через требования. В зрелой кодовой базе они дополняют друг друга, а не конкурируют.

Ключевые принципы

TDD — это не «писать тесты» (это инструмент), а набор принципов, которые в совокупности меняют то, как код рождается. Ниже — формулировка каждого с пояснением, какую конкретную проблему он решает.

Тесты прежде кода (Test-First). Тест пишется до реализации, а не после. Проблема: тесты, написанные постфактум, подтверждают то, как код написан, а не то, как он должен себя вести. Они «зелёные от рождения» и страхуют мало. Тест, написанный первым, фиксирует спецификацию, и именно реализация подстраивается под неё, а не наоборот.

Дизайн через тестируемость. Главное следствие Test-First: код, который трудно протестировать, трудно и написать первым. Разработчик волей-неволей проектирует слабосвязанные компоненты с явными зависимостями — потому что только такой код можно проверить изолированно. TDD, таким образом, не «добавляет тесты», а вынуждает лучший дизайн. Это и есть смысл фразы Бека: «TDD — это про анализ и дизайн, а не про тестирование».

Малые шаги. Цикл длится минуты, а не часы. Каждая итерация добавляет крошечный, проверяемый кусочек поведения. Проблема: большие порции кода накапливают неопределённость — ошибка могла быть сделана давно, и локализовать её тяжело. Малый шаг + немедленный зелёный цвет даёт узкий момент «где точно ошибка — в последней написанной строке».

Минимально достаточный код (YAGNI на шаге Green). На фазе Green пишется ровно столько кода, сколько нужно, чтобы тест прошёл — не больше. Никаких «на будущее», никаких абстракций «про запас». Проблема: спекулятивный код не покрыт тестами, не востребован и превращается в технический долг.

Рефакторинг обязателен, не опционален. Многие воспринимают Refactor-фазу как «если останется время». В строгом TDD это не так: без постоянного рефакторинга код быстро обрастает дублированием (результат «минимально достаточного» подхода на шаге Green), и все выгоды от тестов съедаются поддержкой. Зелёный цвет делает рефакторинг безопасным.

Изолированные, быстрые тесты. Юнит-тесты в TDD запускаются за секунды и проверяют одну единицу в изоляции (с подменами зависимостей — моками/стабами). Проблема: медленные тесты, требующие базы/сети/UI, не дают работать коротким циклом. Слоистость тестов — отдельная тема; подробнее — в пирамиде тестирования.

Один логический assert на тест (как ориентир). Не жёсткое правило, но сильная эвристика: тест, проверяющий одно поведение, проще читать, имя точнее отражает намерение, а падение сразу указывает на нарушенный контракт. Это способствует читаемости спецификации.

Принципы — система, а не меню: Test-First без рефакторинга даёт работающий, но грязный код; малые шаги без изолированных быстрых тестов невыносимо медленны; «минимально достаточный код» без постоянного рефакторинга плодит копипасту. Частичное внедрение создаёт дыры; полное — даёт заявленный эффект.

Как это работает

Рассмотрим цикл на минимальном примере. Задача: функция discount(price, percent), возвращающая цену со скидкой. Цель — показать механику, а не научить языку; код намеренно тривиален.

Итерация 1.

Red. Пишем первый тест:

def test_discount_applies_percent():
    assert discount(100, 10) == 90

Тест падает: функция discount не существует. Причина падения — правильная (нет реализации), а не синтаксическая ошибка в тесте.

Green. Пишем минимально достаточный код:

def discount(price, percent):
    return price - price * percent / 100

Тест проходит. Зелёный.

Refactor. Дублирования пока нет, улучшать нечего. Переходим дальше.

Итерация 2 — добавляем граничный случай.

Red. Пишем тест на нулевой процент:

def test_discount_zero_percent():
    assert discount(100, 0) == 100

Он уже проходит текущей реализацией — это не «красный». По правилу, такой тест не добавляет ценности и обычно не пишется; он полезен только как документация о граничном поведении. Опускаем.

Итерация 3 — новое поведение.

Red. Требование: скидка не может сделать цену отрицательной.

def test_discount_never_negative():
    assert discount(50, 200) == 0

Тест падает (текущий код вернёт -50). Зафиксирована спецификация.

Green. Минимальная правка:

def discount(price, percent):
    result = price - price * percent / 100
    return max(result, 0)

Зелёный.

Refactor. Выносим вычисление в именованную переменную — поведение не изменилось, читаемость выросла. Тесты остаются зелёными — это и есть страховка рефакторинга.

На этом примере видна суть цикла: каждый шаг добавляет одно поведение, подтверждённое одним падающим тестом, и немедленно страхуется рефакторингом под зелёным цветом. В реальной задаче итераций будут десятки; их сумма и есть реализация, причём каждая строка production-кода существует только потому, что её потребовал какой-то тест.

Важная особенность, не очевидная снаружи: TDD не гарантирует отсутствия багов. Тесты покрывают ровно то поведение, о котором разработчик догадался написать. TDD гарантирует другое — регрессионную безопасность: однажды зафиксированное поведение нельзя случайно сломать, не увидев красный тест. Это превращает большой страх изменений в малый и локальный.

Влияние на команду и процесс

TDD часто обсуждают как личную практику разработчика. Это ошибка: устойчивый эффект возникает только тогда, когда TDD встроен в командный процесс. Ниже — что именно меняется, когда цикл Red-Green-Refactor становится нормой. Этот блок адресован прежде всего руководителям.

Тестопригодность как архитектурный драйвер. Главная ценность TDD — не «больше тестов», а лучший дизайн. Команда, пишущая тесты первыми, систематически получает слабосвязанный код с явными зависимостями, потому что только такой код проверяем в изоляции. Архитектурные решения (выбор уровня модульности, где проходят границы, как внедряются зависимости) начинают диктоваться не эстетикой, а тестируемостью. Это напрямую перекликается с эволюционной архитектурой, где фитнес-функции (включая тесты) задают допустимую форму системы.

Shift-left QA. Обнаружение дефектов смещается с этапа тестирования (см. SDLC) на момент написания кода. Дефект локализуется в последней написанной строке, а не в релиз-кандидате через две недели. Это снижает стоимость исправления (чем позже найден дефект, тем дороже) и разгружает QA-функцию от рутинных регрессий, позволяя сосредоточиться на исследовательском тестировании. Подробнее о роли QA — во введении в тестирование.

Планирование и оценка: медленнее «вперёд», быстрее «потом». TDD ощутимо замедляет написание первой работающей версии (накладные расходы на тесты и цикл) и ощутимо ускоряет всё последующее — изменение, расширение, исправление. Для руководителя это означает: метрики velocity в первые спринты проседают, а lead time для изменений и стабильность в долгую — растут. Оценивать TDD по скорости первых итераций — типичная и разрушительная ошибка.

Coverage как gate в CI. В команде, принявшей TDD, покрытие тестами становится не отчётным показателем, а контрольной точкой в конвейере: сборка падает, если новый код не покрыт (или если покрытие просело). Это техническое воплощение дисциплины: «код без теста не попадает в основную ветку». Здесь же — естественная точка применения политики нуля дефектов для работы с backlog’ом найденных проблем.

Ревью тестов раньше кода. На code review акцент смещается: в первую очередь смотрят на тесты — что именно они проверяют, какое поведение специфицировали, насколько читаемы их имена. Production-код ревьюится во вторую очередь, потому что «если тесты хороши, код вынужден быть разумным». Это меняет культуру ревью: от поиска багов глазами — к аудиту спецификаций.

Синергия с парным и коллективным программированием. Цикл Red-Green-Refactor особенно эффективен в pair programming и mob-режиме: один разработчик ведёт («пишет падающий тест»), второй отвечает за green-фазу, затем рефакторят вместе. Это распределение ролей естественно ложится на короткий цикл и усиливает передачу знаний.

Onboarding и живая документация. Хорошо названные тесты (test_discount_never_negative вместо test1) читаются как исполняемая спецификация. Новому разработчику проще понять «что этот модуль вообще делает», читая тесты, а не комментарии. Это снижает порог входа в кодовую базу и косвенно повышает bus-factor проекта.

Синергия с AI-генерацией кода. TDD — естественный партнёр для AI-Driven Development: именно тесты, написанные первыми, служат исполняемой спецификацией, по которой AI генерирует реализацию, и тем же gate-ом, который эту реализацию валидирует. Подробнее об этой связи — в статье про AIDD.

Преимущества

Регрессионная безопасность. Однажды зафиксированное поведение нельзя незаметно сломать. Любое изменение, ломающее контракт, немедленно даёт красный тест. Это превращает страх «а не задену ли я что-то важное» из глобального в локальный.

Чистый дизайн как побочный продукт. Слабая связанность, явные интерфейсы, малые ответственности компонентов — не цель, а следствие. Код, который трудно тестировать, трудно и написать первым; TDD систематически отсеивает «труднотестируемый» дизайн.

Живая, исполняемая документация. Именованные тесты описывают поведение на языке, который не может устареть (если тест зелёный — он актуален). Это дополняет статичную документацию и снижает риск рассинхронизации «доки vs код».

Уверенность рефакторить. Зелёный набор тестов — страховка, без которой масштабный рефакторинг парализует команду. Именно TDD делает безопасным правило бойскаута («оставь код чище, чем нашёл») и контроль технического долга.

Снижение дефектов — с оговорками. Ряд исследований и индустриальных отчётов (Nagappan, Williams, Maximilien; исследования IBM/Microsoft в 2000-х) связывает TDD со снижением плотности дефектов на 40–90% при умеренном росте времени разработки (15–35%). Цифры варьируются от исследования к исследованию, и зависят от зрелости команды; но качественный вывод стабилен: при правильном применении дефектов меньше, а их средняя стоимость — ниже, потому что ловятся они рано.

Ускорение в долгую. Несмотря на замедление на старте, зрелая TDD-кодовая база меняется быстрее и безопаснее нетестируемой, потому что изменения не требуют долгого ручного регрессионного тестирования и не плодят скрытых поломок.

Недостатки и риски

Иллюзия медленного старта. Самая частая критика — «TDD замедляет». На уровне первых итераций это правда; проблема возникает, когда руководитель принимает решение по метрикам короткого окна, не дождавшись компенсации в долгую. Это управленческая, а не техническая ошибка.

Хрупкие тесты (flaky / brittle). Тесты, привязанные к деталям реализации (а не к поведению), ломаются при любом рефакторинге и подрывают доверие к набору. Симптом: «red-шум», при котором красные тесты игнорируются. Это следствие слабого навыка, а не метода, но встречается массово.

Over-mocking. Чрезмерное использование моков создаёт тесты, которые зелёные, но не проверяют ничего, кроме того, что код вызывает другие методы в ожидаемом порядке. Такие тесты не страхуют поведение и сломаются при любом безобидном изменении. Это снова вопрос навыка проектирования тестов.

Тестирование реализации, а не поведения. Если разработчик тестирует «как сделано», а не «что должно происходить», рефакторинг становится невозможным — тесты держат код мёртвой хваткой. Граница между двумя стилями тонка и требует опыта.

Ложная уверенность. Зелёный набор тестов не означает отсутствие багов — он означает лишь, что не сломано то, о чём догадались написать. Не покрытое тестами поведение (о нём не подумали) остаётся незастрахованным. «Тесты проходят» ≠ «код правильный».

Стоимость поддержки. Большой набор тестов — это кодовая база, которую нужно поддерживать наравне с production-кодом. Рефакторинг тестов, актуализация при изменении требований, починка хрупких — отдельная статья расходов, которую нужно закладывать в планирование.

Не везде применимо. TDD плохо работает там, где поведение трудно выразить или быстро проверить: чистая вёрстка UI, исследовательские задачи и прототипы (spike), визуальные/эстетические аспекты, интеграции с нестабильными внешними системами. Подробнее — в следующем разделе.

Требует навыка и дисциплины. TDD — не «брось тесты, и станет лучше». Эффективность требует обучения: умения проектировать тестируемый код, писать читаемые тесты, чувствовать грань между тестированием поведения и реализации. В команде без этого навыка внедрение часто даёт «TDD-theatre» — формальное наличие тестов без эффекта.

Когда использовать

  • Сложная доменная логика. Правила расчётов, конечные автоматы, алгоритмы — там, где поведение состоит из множества ветвлений и легко что-то сломать незаметно. Тесты здесь дают максимальную отдачу.
  • Долгоживущие кодовые базы. Если код будет меняться годами, инвестиция в регрессионную страховку окупается многократно; для одноразовых скриптов — почти никогда.
  • Дисциплинированная команда. TDD требует инженерной зрелости; в команде с устоявшейся культурой качества цикл приживается естественно.
  • Есть CI с автоматическим запуском тестов. Без быстрой обратной связи в конвейере ценность зелёного набора резко падает — тесты должны запускаться на каждом коммите.
  • Зрелые компоненты в итеративной или Agile разработке. TDD естественно ложится на короткие итерации с частой обратной связью, дополняя Scrum инженерной дисциплиной.
  • Код под AI-генерацию. При использовании AIDD тесты, написанные первыми, становятся исполняемой спецификацией и gate-ом для AI-сгенерированного кода.
  • Регулируемые домены с высокими требованиями к прослеживаемости. Там, где важно показать «как именно проверено каждое требование», тесты-как-спецификация дают аудируемый след.

Когда НЕ использовать

  • Одноразовые прототипы и spike-исследования. Если цель — проверить гипотезу и выбросить код, TDD даёт чистые убытки. Здесь уместен подход «сделать быстро», а к production-реализации — вернуться с тестами.
  • Чистая вёрстка UI и эстетика. Поведение, которое трудно или долго выразить в автоматическом тесте (раскладка, визуальное восприятие), плохо поддаётся циклу. Здесь работают другие практики — визуальное регрессионное тестирование, ручное QA.
  • Исследовательские задачи и R&D. Когда заранее неизвестно, что именно должно получиться, фиксировать поведение в тестах преждевременно — тесты будут переписываться вслед за меняющимся пониманием.
  • Команда без навыка и без времени на обучение. Внедрение TDD «с понедельника» в команде без инженерной базы даёт формальные тесты и разочарование. Сначала — обучение и пилот; потом — масштабирование.
  • Жёсткие «горящие» дедлайны. Когда процессом управляет паника (deadline-driven), TDD первым делом отбрасывается как «лишнее». Это симптом больного процесса, но реалистично: в таком режиме TDD не приживётся, пока не устранена первопричина.

Связанные подходы

TDD — не единственный «*DD» в разработке; за разными аббревиатурами стоят разные «движущие силы» (тесты, типы, поведение, домен и т. д.). Статья открывает серию материалов о driven-подходах; ниже — краткая карта, со ссылками на уже опубликованные материалы базы знаний.

  • Extreme Programming (XP) — методология, в рамках которой TDD был изначально сформулирован; TDD — одна из её базовых практик наряду с парным программированием и непрерывной интеграцией.
  • Agile и Scrum — процессные рамки, в которые TDD встраивается как инженерная дисциплина.
  • AIDD — AI-Driven Development: TDD в AIDD играет роль исполняемой спецификации и gate-а для AI-генерируемого кода.
  • SDLC — где TDD живёт внутри жизненного цикла (прежде всего — этапы разработки и тестирования).
  • Пирамида тестирования — как TDD-юниты соотносятся с интеграционными и e2e-тестами.
  • Политика нуля дефектов — как работать с найденными проблемами в кодовой базе, где тесты есть.
  • Технический долг и правило бойскаута — TDD делает безопасным управление долгом и постоянное улучшение кода.
  • Эволюционная архитектура — тесты как фитнес-функции, задающие допустимую форму системы.
  • ADR — фиксация архитектурных решений, в том числе о выборе TDD как практики команды.
  • BDD — Behaviour Driven Development — родственный подход, где движущей силой выступает поведение, описанное на общем языке в сценариях Given-When-Then. BDD надстраивается над TDD: задаёт поведение на уровне фичи, а TDD страхует реализацию на уровне юнитов.
  • FDD — Feature Driven Development — организационная методология, где движущей силой выступает клиенто-ориентированная фича. FDD и TDD дополняют друг друга: FDD задаёт процесс и планирование на уровне организации, TDD страхует реализацию отдельной фичи на уровне кода.
  • MDD — Model Driven Development — родственный подход, где движущей силой выступает формальная модель. MDD и TDD не конкурируют: MDD задаёт модель как источник истины для кодогенерации, TDD страхует поведение сгенерированного кода тестами.
  • TDD — Type Driven Developmentта же аббревиатура TDD, но принципиально иной подход из функционального программирования: движущая сила — система типов, а «зелёный» означает «код компилируется», а не «тесты проходят». Два подхода не конкурируют, а дополняют друг друга: типы страхуют структуру и инварианты, тесты — поведение и конкретные значения.

Родственные driven-подходы — ATDD — Acceptance Test Driven Development (критерии приёмки на уровне требований), BDD — Behaviour Driven Development (поведение и общий язык), DDD — Domain Driven Design (домен как движущая сила), FDD — Feature Driven Development (фича как единица доставки), MDD — Model Driven Development (формальная модель как источник истины), а также «сатирические» варианты вроде Panic Driven Development — рассматриваются в отдельных статьях серии.

Краткий вердикт для руководителя

TDD — это инвестиция в долгосрочную изменяемость кодовой базы, а не способ писать код быстрее здесь и сейчас. Эффект: меньше дефектов, дешевле рефакторинг, выше онбординг-скорость, более слабосвязанная архитектура. Цена: замедление на старте, необходимость обучать команду, поддержка набора тестов как отдельного актива. Берите, если кодовая база долгоживущая, логика сложная, а команда готова к инженерной дисциплине. Не берите, если горизонт — недели, задача — одноразовый прототип, или процессом управляет паника дедлайнов. Оценивать эффект TDD на горизонте одного спринта — главная ошибка внедрения; эффект проявляется на горизонте месяцев и лет.

Источники и материалы

Первоисточники и ключевые публикации

  • Kent Beck. Test-Driven Development: By Example. Addison-Wesley, 2002 — фундаментальный первоисточник, формулировка цикла Red-Green-Refactor и духа методологии.
  • Robert C. Martin (Uncle Bob). The Three Laws of TDD — каноническая формулировка трёх законов; доклад и ряд статей.
  • Robert C. Martin. Clean Code: A Handbook of Agile Software Craftsmanship (2008) — глава о TDD и тестах как части чистого кода.
  • Martin Fowler. TestDrivenDevelopment (martinfowler.com) — анализ сути TDD, разведение Test-First и Test-After.
  • Nagappan, Maximilien, Williams, Vouk. Realizing quality improvement through test-driven development: results and experiences of four industrial teams (Empirical Software Engineering, 2008) — отраслевое исследование влияния TDD на дефекты.
  • George, Williams. A Structured Experiment of Test-Driven Development (ISSTA 2006 / IEEE) — данные о снижении дефектности при росте времени разработки.
  • Dan North. Introducing BDD (Better Software, 2006) — исторический переход от TDD/ATDD к BDD; помогает понять границу практик.

Связанные материалы базы знаний