Разработка через тестирование (TDD): пошаговое руководство
Практика3 минРедакция Innovialabs
Коротко
TDD — это короткий цикл: сначала пишете падающий тест, затем минимальный код, чтобы он прошёл, и после этого улучшаете код под защитой тестов.
Что такое TDD в двух словах
Test-Driven Development — подход, при котором тест пишется раньше кода. Работа идёт короткими циклами из трёх шагов:
- Red — пишете тест на маленькое поведение, запускаете и видите, что он падает.
- Green — пишете минимальный код, чтобы тест прошёл. Не красиво, а просто.
- Refactor — улучшаете структуру кода и тестов, не меняя поведения. Тесты должны остаться зелёными.
Один цикл занимает минуты. Смысл не в «покрытии», а в том, что каждый кусок кода появляется в ответ на конкретное требование.
Пример: стоимость корзины с промокодом
Задача: посчитать итог корзины, промокод SALE10 даёт скидку 10%. Используем Python и pytest.
Цикл 1, red. Начинаем с самого простого случая:
# test_cart.py
from cart import cart_total
def test_empty_cart_costs_zero():
assert cart_total([]) == 0
Тест падает: модуля cart ещё нет. Это правильный красный — падение по ожидаемой причине.
Green. Минимальный код:
# cart.py
def cart_total(items):
return 0
Да, это «жульничество». Но следующий тест заставит написать настоящую логику.
Цикл 2. Добавляем тест на сумму позиций:
def test_sums_price_times_quantity():
items = [{"price": 100, "qty": 2}, {"price": 50, "qty": 1}]
assert cart_total(items) == 250
Красный. Делаем зелёным:
def cart_total(items):
return sum(i["price"] * i["qty"] for i in items)
Цикл 3. Промокод:
def test_sale10_gives_ten_percent_off():
items = [{"price": 200, "qty": 1}]
assert cart_total(items, promo="SALE10") == 180
def test_unknown_promo_is_ignored():
items = [{"price": 200, "qty": 1}]
assert cart_total(items, promo="WRONG") == 200
Green и refactor. После того как тесты прошли, выносим скидки в словарь, чтобы новые промокоды добавлялись одной строкой:
DISCOUNTS = {"SALE10": 0.10}
def cart_total(items, promo=None):
subtotal = sum(i["price"] * i["qty"] for i in items)
return subtotal * (1 - DISCOUNTS.get(promo, 0))
Все четыре теста зелёные — рефакторинг безопасен. Для денег в реальном проекте стоит использовать Decimal или целые копейки, а не float; это тоже удобно зафиксировать отдельным тестом.
Где TDD окупается
- Бизнес-логика с понятными правилами: расчёты, скидки, тарифы, валидация, статусы заказов.
- Исправление багов: сначала тест, который воспроизводит ошибку, потом фикс. Баг больше не вернётся незаметно.
- Код, который часто меняется: тесты дают смелость рефакторить.
- Публичные API и библиотеки: тест заставляет сначала подумать об удобстве интерфейса.
Где TDD мешает
- Прототипы и эксперименты, которые завтра выбросят. Сначала выясните, что строить.
- Вёрстка и визуальная часть: результат проверяется глазами, а не assert.
- Тонкие обёртки над внешними сервисами: там полезнее интеграционные тесты.
- Незнакомая технология: сначала короткий spike без тестов, потом TDD на чистовой версии.
TDD — инструмент, а не религия. Многие команды применяют его для ядра логики и пишут тесты «после» для остального.
Частые ошибки новичков
- Слишком большой шаг. Тест сразу на всю фичу — и вы полчаса в красном. Дробите до одного поведения.
- Пропуск красной фазы. Если не видели падения, тест может проверять не то или не запускаться вовсе.
- Пропуск рефакторинга. Без третьего шага TDD быстро даёт работающий, но запутанный код.
- Тесты на реализацию. Проверяйте результат и поведение, а не то, какие приватные методы вызваны.
- Лишние моки. Если для теста нужно подменить пять зависимостей, это сигнал о проблеме в дизайне кода.
Как начать в команде
- Выберите один модуль с чёткими правилами.
- Договоритесь, что каждый баг сначала закрывается тестом.
- Подключите запуск тестов в CI, чтобы красная сборка блокировала слияние.
- Через пару недель обсудите, где подход помог, а где был лишним.