TDD разработка. Как не завалить проект, опираясь на результаты тестов?
TDD, или Test-Driven Development, — это подход к разработке, при котором программист сначала пишет проверку для будущего кода, а уже потом сам код. На первый взгляд звучит немного странно: «Как можно тестировать то, чего ещё нет?» Но именно в этом и заключается идея — сначала сформулировать ожидаемое поведение программы, а потом заставить программу этому поведению соответствовать.
Главная мысль TDD: тест становится не последним этапом разработки, а инструментом проектирования кода.
Эта тема важна не только для опытных разработчиков. TDD помогает новичкам лучше понимать задачу, командам — договариваться о поведении системы, а бизнесу — снижать риск появления дорогих ошибок. Правда, TDD не является волшебной таблеткой и не превращает любой проект в безупречный шедевр. Зато он помогает не строить программный «дом» на фундаменте из скотча, надежды и комментария «потом поправим».
- ВАЖНО! Что меняет ИИ? AI-driven TDD
- ИИ как помощник на этапе «Красный»
- ИИ как ускоритель этапа «Зелёный»
- ИИ как ревьюер и генератор новых сценариев
- Что такое TDD разработка
- Предыстория
- Расшифровка и перевод
- TDD в программировании
- TDD в тестировании
- Простыми словами
- Этапы разработки согласно TDD
- Красный
- Зелёный
- Синий
- Откуда такие названия?
- Три правила Кента Бека
- Не пишите производственный код, пока не написан тест
- Пишите ровно столько тестового кода, чтобы увидеть ошибку
- Пишите ровно столько производственного кода, чтобы прошёл один тест
- Пример реализации TDD — пишем калькулятор НДС на PHP
- 🔴 Красный — пишем тест, который не проходит.
- 🟢 Зелёный — пишем минимальный код, чтобы тест прошёл.
- 🔵 Синий — улучшаем код, не нарушая работу тестов.
- Преимущества TDD
- Недостатки TDD
- TDD и другие подходы
- TDD и SDD
- TDD и BDD
- Краткие итоги
ВАЖНО! Что меняет ИИ? AI-driven TDD
Искусственный интеллект заметно меняет практику TDD: он умеет помогать писать тесты, находить пограничные случаи, объяснять падения и предлагать варианты реализации. Однако ИИ не отменяет сам подход. Скорее, он ускоряет отдельные шаги цикла, но ответственность за корректность требований и результата остаётся у человека.
ИИ способен быстро написать тест, но только человек должен решить, правильно ли этот тест описывает бизнес-смысл.
ИИ как помощник на этапе «Красный»
Разработчик может описать ИИ задачу обычными словами: «Нужно рассчитать НДС 20% для цены без налога, а для нулевой ставки вернуть исходную сумму». Модель предложит набор тестов, включая базовые, пограничные и ошибочные сценарии. Это полезно, потому что человек часто думает только о счастливом пути, а программа особенно любит жить на краях диапазона.
Но сгенерированные тесты нельзя принимать на веру. ИИ может неверно понять округление, перепутать цену с суммой, применить не ту налоговую ставку или придумать правило, которого нет в требованиях. Поэтому тест нужно проверять как договор: если он неправильный, система может честно пройти неправильную проверку.
ИИ как ускоритель этапа «Зелёный»
После появления падающего теста ИИ может предложить минимальную реализацию, объяснить синтаксис или показать несколько способов исправления ошибки. Особенно хорошо это работает для рутинного кода: преобразований данных, простых валидаторов, DTO, адаптеров и стандартных CRUD-операций.
При этом важно не превращать разработку в механическое копирование ответа из чата. Хорошая практика — попросить ИИ объяснить каждую строку, назвать возможные побочные эффекты и предложить более простой вариант. Если разработчик не понимает код, он не становится качественным только потому, что его написал искусственный интеллект.
ИИ как ревьюер и генератор новых сценариев
На этапе рефакторинга ИИ может искать дублирование, слишком сложные условия, подозрительные имена и потенциальные ошибки. Также ему можно дать существующий набор тестов и попросить придумать сценарии, которых не хватает: пустые значения, большие числа, отрицательные суммы, повторные вызовы, неверные форматы и неожиданные комбинации параметров.
Но окончательное решение должно принимать команда. ИИ не знает всех ограничений продукта, юридических требований, особенностей клиентов и негласных договорённостей проекта. Поэтому современный вариант TDD выглядит так: человек формулирует смысл, ИИ помогает расширить варианты, а автоматические тесты и человеческое ревью проверяют результат.
Что такое TDD разработка
TDD — это сокращение от английского Test-Driven Development, то есть «разработка через тестирование» или «разработка, управляемая тестами». Подход предполагает короткие повторяющиеся циклы: сначала создаётся тест, затем пишется минимальный код для его прохождения, после чего код улучшается.
TDD — это не «писать больше тестов», а сначала описывать ожидаемое поведение программы проверяемым способом.
Предыстория
Идеи, похожие на TDD, существовали задолго до появления самого термина. Разработчики всегда проверяли программы, писали небольшие тестовые сценарии и пытались убедиться, что изменения не ломают старую функциональность. Однако раньше тестирование часто выполнялось в конце проекта — когда основная архитектура уже была построена.
Популярность современному TDD придал Кент Бек, один из создателей методологии экстремального программирования, или Extreme Programming. В начале 2000-х годов TDD стали активно обсуждать как практику, которая помогает проектировать простой код маленькими шагами и получать постоянную обратную связь.
Расшифровка и перевод
Буквально Test-Driven Development переводится как «разработка, управляемая тестами». Иногда используют варианты «разработка через тестирование» или «тест-ориентированная разработка». Все они передают общий смысл, хотя дословно ни один вариант не звучит идеально.
Важное слово здесь — driven, то есть «управляемая». Тест в TDD не просто проверяет уже готовую функцию. Он влияет на то, какой интерфейс будет у функции, какие аргументы она принимает, что возвращает и как ведёт себя в ошибочных ситуациях.
TDD в программировании
В программировании TDD применяется при разработке функций, классов, модулей и компонентов. Сначала программист формулирует небольшой сценарий: например, «если передать цену 100 и ставку НДС 20%, результат должен быть 120». Затем этот сценарий записывается в виде автоматического теста.
Если тест не проходит, это нормально: кода ещё нет или он недостаточен. После этого создаётся простейшая реализация, которая делает тест зелёным. Затем код можно аккуратно переработать, не опасаясь потерять уже достигнутый результат.
TDD в тестировании
В классическом понимании тестирование часто воспринимается как отдельный этап контроля качества. В TDD тестирование тесно связано с проектированием. Пока разработчик пишет тест, он вынужден заранее подумать о том, как будет использоваться код.
Например, неудобный для тестирования класс часто оказывается неудобным и в обычной работе: он требует слишком много зависимостей, делает несколько дел сразу или прячет важную логику внутри побочных эффектов. Тест в таком случае выступает не только как проверка, но и как сигнал: «архитектуру стоит упростить».
Простыми словами
Представим, что вы собираете шкаф. Можно сначала скрутить всё подряд, а в конце попытаться понять, открываются ли дверцы. А можно после каждого небольшого шага проверять: полка держится, дверца закрывается, винты не торчат, конструкция не падает. TDD — это второй вариант, только вместо шкафа у нас программа, а вместо дверцы — тест.
Практический смысл можно выразить так:
- Сначала сформулировать, что программа должна делать.
- Затем написать автоматическую проверку.
- После этого реализовать минимально необходимый код.
- В конце улучшить структуру, не меняя ожидаемое поведение.
| Обычный подход | TDD-подход |
|---|---|
| Сначала код, тесты позже | Сначала тест, затем код |
| Ошибки могут обнаружиться в конце | Ошибки выявляются маленькими шагами |
| Тест описывает уже принятое решение | Тест помогает принять решение о дизайне |
| Часто проверяется большой сценарий | Чаще проверяется маленькое поведение |
Этапы разработки согласно TDD
Классический цикл TDD состоит из трёх этапов: Красный, Зелёный и Синий. В англоязычной литературе обычно используются названия Red, Green, Refactor. В русскоязычной среде третий этап иногда называют «рефакторингом», а иногда — «синим» по цветовой схеме цикла.
Цикл TDD должен быть коротким: один небольшой тест — одна небольшая реализация — одно безопасное улучшение.
Красный
На этапе «Красный» разработчик пишет тест для поведения, которого ещё нет или которое пока работает неправильно. Затем тест запускается и должен завершиться ошибкой. Это важный момент: если новый тест сразу зелёный, возможно, он ничего не проверяет или проверяет уже существующее поведение.
Красный тест должен быть понятным. Из него желательно сразу видеть, что требуется от системы. Например: «при передаче суммы 100 и ставки 20% функция возвращает 120». Если тест сложно сформулировать, это может означать, что сама задача недостаточно ясна.
Зелёный
На этапе «Зелёный» пишется минимальный код, который заставляет тест пройти. Слово «минимальный» здесь принципиально важно. Не нужно сразу проектировать универсальную платформу расчётов, если текущая задача — умножить число на 1,2.
Иногда новичок пытается сделать код идеальным с первой попытки и тратит часы на будущие возможности. TDD предлагает более спокойный порядок: сначала доказать тестом нужное поведение, затем постепенно расширять решение.
Синий
«Синий» — это этап улучшения кода, который чаще называют рефакторингом. Поведение программы не меняется, но внутренняя структура становится лучше: устраняется дублирование, уточняются имена, упрощаются условия, разделяются ответственности.
Рефакторинг безопаснее проводить тогда, когда тесты уже проходят. Если после изменения какой-то тест стал красным, значит, либо код случайно изменил поведение, либо сам тест больше не соответствует актуальным требованиям. В обоих случаях у команды есть конкретный сигнал для анализа.
Откуда такие названия?
Цвета описывают состояние тестов. «Красный» означает, что проверка не прошла. «Зелёный» — все ожидаемые проверки успешны. Третий цвет нужен для обозначения приведения кода в порядок без изменения внешнего результата.
Получается своеобразный светофор для разработчика: красный — остановились и увидели задачу, зелёный — можно двигаться, синий — убираем лишние детали и готовимся к следующему кругу. Цикл может повторяться десятки раз за день.
| Этап | Что делает разработчик | Результат |
|---|---|---|
| Красный | Пишет тест, описывающий новое поведение | Тест не проходит |
| Зелёный | Добавляет минимальную реализацию | Тест проходит |
| Синий | Улучшает внутреннюю структуру | Тесты по-прежнему проходят |
Три правила Кента Бека
Кент Бек сформулировал знаменитые правила TDD. Они выглядят простыми, но требуют дисциплины. Их задача — не дать разработчику перескочить через этапы и написать слишком много кода до появления проверяемого результата.
Правила ограничивают не творчество программиста, а количество неопределённости на каждом шаге.
Не пишите производственный код, пока не написан тест
Первое правило означает, что новый рабочий код должен появляться в ответ на конкретный падающий тест. Это помогает не реализовывать функции «на всякий случай» и не создавать сложные абстракции без подтверждённой потребности.
Правило не стоит понимать чрезмерно догматично. В реальном проекте есть конфигурация, инфраструктурный код, миграции и другие элементы, которые не всегда удобно создавать через классический unit-тест. Но для бизнес-логики это правило остаётся очень полезным ориентиром.
Пишите ровно столько тестового кода, чтобы увидеть ошибку
Не нужно сразу создавать двадцать сценариев, если один тест уже показывает отсутствие нужной функции. Сначала достаточно минимальной проверки, которая гарантированно не проходит.
Это помогает удерживать маленький размер шага. Разработчик не тонет в огромном наборе предположений и быстрее получает обратную связь. Следующий тест можно добавить после того, как предыдущая версия стала зелёной.
Пишите ровно столько производственного кода, чтобы прошёл один тест
После появления красного теста следует написать только необходимую реализацию. Не нужно заранее обрабатывать все возможные форматы, подключать пять паттернов проектирования и создавать фабрику фабрик. Сначала — самое маленькое решение.
Когда появятся новые тесты, код будет постепенно развиваться. В этом есть практическая польза: архитектура формируется под реальные требования, а не под фантазии о том, что однажды может понадобиться.
Пример реализации TDD — пишем калькулятор НДС на PHP
Рассмотрим простой пример на PHP. Наша задача — создать функцию, которая получает цену без НДС и ставку налога, а возвращает цену с НДС. Для простоты будем считать, что ставка передаётся в процентах: 20 означает 20%, 10 означает 10%, 0 означает отсутствие налога.
Практический смысл примера: мы не пытаемся сразу создать бухгалтерскую систему, а пошагово закрепляем одно небольшое правило.
🔴 Красный — пишем тест, который не проходит.
Начнём с теста. В реальном PHP-проекте можно использовать PHPUnit. Тест проверяет, что цена 100 при ставке 20% превращается в 120. На этом этапе класса и метода может ещё не существовать — поэтому тест закономерно будет красным.
<?php use PHPUnitFrameworkTestCase; final class VatCalculatorTest extends TestCase { public function testAddsTwentyPercentVat(): void { $calculator = new VatCalculator(); $result = $calculator->priceWithVat(100, 20); $this->assertSame(120.0, $result); } }
Если запустить тест прямо сейчас, PHPUnit сообщит, что класс VatCalculator не найден. Это ожидаемая ошибка. Мы получили точное описание того, чего не хватает системе, а не абстрактное «что-то где-то не работает».
🟢 Зелёный — пишем минимальный код, чтобы тест прошёл.
Теперь добавим минимальную реализацию. Формула проста: цена с НДС равна цене без НДС плюс цена без НДС, умноженная на ставку, делённую на 100.
<?php final class VatCalculator { public function priceWithVat(float $price, float $rate): float { return $price + ($price * $rate / 100); } }
После запуска теста получаем зелёный результат. Код короткий, но уже выполняет требование. Сейчас не стоит добавлять округление, валюты, разные режимы налогообложения и интеграцию с базой данных. Мы пока решаем только одну задачу.
🔵 Синий — улучшаем код, не нарушая работу тестов.
Теперь можно подумать, какие сценарии ещё важны. Например, ставка 0%, ставка 10%, дробная цена и отрицательные значения. Сначала добавим тест для нулевой ставки. Он должен подтвердить, что цена остаётся прежней.
<?php public function testReturnsOriginalPriceForZeroRate(): void { $calculator = new VatCalculator(); $result = $calculator->priceWithVat(250, 0); $this->assertSame(250.0, $result); }
Затем можно вынести расчёт ставки в отдельную переменную, добавить проверку входных данных или использовать объект денег. Но каждое изменение должно сопровождаться тестом. Например, если бизнес запрещает отрицательную цену, зафиксируем это явно:
<?php public function testRejectsNegativePrice(): void { $calculator = new VatCalculator(); $this->expectException(InvalidArgumentException::class); $calculator->priceWithVat(-10, 20); }
Тогда производственный код можно расширить:
<?php final class VatCalculator { public function priceWithVat(float $price, float $rate): float { if ($price < 0) { throw new InvalidArgumentException('Price cannot be negative'); } return $price + ($price * $rate / 100); } }
Обратите внимание: тесты начинают рассказывать историю требований. Из них понятно, что функция умеет считать налог, поддерживает нулевую ставку и не принимает отрицательную цену. Хороший набор тестов похож на живую документацию, только документацию, которую невозможно случайно оставить устаревшей после запуска CI.
| Сценарий | Входные данные | Ожидаемый результат |
|---|---|---|
| Стандартная ставка | 100 и 20% | 120 |
| Нулевая ставка | 250 и 0% | 250 |
| Другая ставка | 100 и 10% | 110 |
| Ошибка ввода | -10 и 20% | Исключение |
Преимущества TDD
TDD не делает разработку автоматически быстрой, зато делает её более предсказуемой. Польза особенно заметна в проектах, где код часто изменяется, продукт живёт годами, а ошибки дорого обходятся компании.
Основная ценность TDD проявляется не в момент написания первой функции, а через месяцы изменений и расширений проекта.
Раннее обнаружение ошибок. Ошибка находится рядом с местом, где она появилась. Не приходится вспоминать, какие изменения были сделаны три недели назад.
Безопасный рефакторинг. Разработчик может менять внутреннюю структуру кода и быстро проверять, сохранилось ли прежнее поведение.
Более понятный дизайн. Код, который легко тестировать, часто имеет небольшие классы, явные зависимости и разделённые обязанности.
Живая документация. Тесты показывают, как пользоваться компонентом и какие результаты считаются правильными.
Меньше страха перед изменениями. Команда понимает, что важные сценарии защищены автоматическими проверками.
Удобная работа в команде. Тесты помогают согласовать ожидания между разработчиками, аналитиками и QA-специалистами.
Быстрая обратная связь. Unit-тесты обычно выполняются за секунды и не требуют ручного кликанья по интерфейсу.
Лучшее понимание требований. Чтобы написать тест, приходится уточнять спорные моменты: что делать при нулевом значении, ошибке, пустом списке или повторном запросе.
Недостатки TDD
У TDD есть ограничения, и честный разговор о них важнее рекламных обещаний. Подход требует времени, дисциплины и определённого опыта. Иногда команда пишет формально зелёные тесты, которые почти не защищают систему, и затем делает вывод, что TDD «не работает».
TDD не компенсирует плохие требования: можно идеально протестировать неправильное решение.
Дополнительные затраты на старте. На небольшую функцию нужно написать тест, код и иногда рефакторинг. В краткосрочной перспективе это может казаться медленнее.
Сложность обучения. Новичку непривычно специально писать код, который сначала должен упасть. Возникает ощущение, будто программа «ломается по плану».
Не всё удобно тестировать одинаково. Интерфейсы, интеграции с внешними сервисами, графику и сложные распределённые процессы нельзя полностью покрыть быстрыми unit-тестами.
Риск тестировать реализацию вместо поведения. Если тест слишком сильно привязан к внутренним методам и структуре классов, любое улучшение кода приводит к массовому переписыванию тестов.
Стоимость поддержки. Тесты тоже являются кодом. Они могут устаревать, дублироваться, содержать ошибки и требовать рефакторинга.
Ложное чувство безопасности. Большой процент покрытия строками не гарантирует, что проверены важные бизнес-сценарии. Можно покрыть тестами даже бесполезный код.
Необходимость хорошего дизайна тестов. Плохие тесты могут быть медленными, нестабильными и зависеть друг от друга. Тогда команда начнёт их отключать, а это уже путь к цифровому болоту.
TDD и другие подходы
TDD не существует в вакууме. На практике его используют вместе с другими способами анализа требований, проектирования и проверки поведения. Особенно часто TDD сравнивают с SDD и BDD.
Подходы не обязательно выбирать «один против другого»: SDD формулирует требования, BDD описывает поведение, а TDD помогает реализовать детали.
TDD и SDD
Specification-Driven Development (SDD) — разработка через спецификацию. Сначала создаётся подробное описание требований, правил, форматов, ограничений и ожидаемых результатов, а затем на основе этой спецификации пишется код.
SDD удобно применять в системах, где важны строгие контракты: платёжные сервисы, государственные интеграции, API, протоколы обмена данными. Спецификация может быть достаточно крупной и формальной, тогда как TDD обычно работает короткими итерациями на уровне отдельных компонентов.
| Критерий | TDD | SDD |
|---|---|---|
| Главный артефакт | Автоматический тест | Спецификация требований |
| Фокус | Проверяемое поведение кода | Полнота и точность требований |
| Типичный масштаб | Функция, класс, модуль | Система, API, бизнес-процесс |
| Сильная сторона | Быстрая обратная связь | Формальный контракт |
TDD и BDD
Behavior-Driven Development (BDD) — разработка через описание поведения системы на примерах. В BDD стараются говорить не столько о классах и методах, сколько о том, что видит пользователь или бизнес.
Типичный сценарий BDD может выглядеть так: «Если клиент положил товар в корзину и перешёл к оформлению заказа, система должна показать стоимость доставки». Такой сценарий понятен не только программисту. Затем он может быть автоматизирован с помощью инструментов, например Cucumber-подобных фреймворков.
TDD чаще начинается с технического уровня: функция принимает аргументы и возвращает результат. BDD начинается с поведения системы и бизнес-примера. На практике они хорошо дополняют друг друга: BDD помогает выбрать правильные сценарии, а TDD — аккуратно реализовать внутренние компоненты.
| Подход | Основной вопрос | Пример формулировки |
|---|---|---|
| TDD | Как должен вести себя компонент? | Метод возвращает 120 при цене 100 и ставке 20% |
| BDD | Что должна делать система для пользователя? | Покупатель видит итоговую цену с налогом |
| SDD | Какие требования и правила обязательны? | API принимает ставку в диапазоне от 0 до 100% |
Краткие итоги
TDD — это подход, при котором тест становится частью процесса проектирования. Разработчик не ждёт окончания работы, чтобы проверить результат, а постоянно движется маленькими циклами: «Красный — Зелёный — Синий».
Хороший TDD — это не культ тестов, а привычка проверять предположения до того, как они превратятся в дорогие ошибки.
Главное, что стоит запомнить:
- сначала описывается ожидаемое поведение;
- падающий тест — нормальная и необходимая часть процесса;
- после теста пишется минимальная реализация;
- рефакторинг выполняется только с защитой проверок;
- тесты должны проверять смысл, а не случайные детали реализации;
- процент покрытия важен, но не заменяет качество сценариев;
- ИИ может ускорить TDD, но не способен полностью заменить понимание требований;
- TDD, BDD и SDD можно сочетать, потому что они отвечают на разные вопросы.
Начинать лучше с небольшой задачи: калькулятора, валидатора, преобразователя данных или простой бизнес-функции. Не нужно сразу переписывать весь проект и объявлять войну коду без тестов. Выберите один сценарий, напишите тест, увидьте красный результат, сделайте его зелёным и только потом улучшайте решение. Через некоторое время такой цикл перестанет казаться церемонией и станет обычной рабочей привычкой — примерно как проверять, взяли ли вы ключи, прежде чем захлопнуть дверь.
