TDD: red-green-refactor
TDD на Python: цикл red-green-refactor на живом примере.
TDD (Test-Driven Development) — подход, при котором сначала пишут падающий тест, затем минимальный код, чтобы он прошёл, и только потом улучшают код.
Цикл из трёх шагов
- Red (красный). Пишем тест на ещё не существующее поведение — он падает. Это нормально: тест задаёт цель.
- Green (зелёный). Пишем минимальный код, чтобы тест прошёл. Не больше, чем нужно.
- Refactor (рефакторинг). Улучшаем код, не меняя поведения. Тесты держат нас в безопасности — если что-то сломаем, увидим сразу.
Затем цикл повторяется для следующего требования. Тесты ведут разработку — отсюда и название.
Зачем писать тест первым
- Вы сначала думаете о поведении (что функция должна делать), а не о реализации.
- Гарантированно нет непротестированного кода — тест существовал до кода.
- Тест точно проверяет нужное: вы видели, как он падает, значит он работает.
Живой пример: функция roman() — арабские в римские
Будем строить функцию по шагам. Шаг Red: пишем тесты на ещё не написанную логику. Чтобы увидеть «красный» прямо в браузере, дадим заведомо неполную заготовку:
import unittest
def roman(n):
return "" # пока заглушка — тесты должны упасть
class TestRoman(unittest.TestCase):
def test_one(self):
self.assertEqual(roman(1), "I")
def test_four(self):
self.assertEqual(roman(4), "IV")
def test_nine(self):
self.assertEqual(roman(9), "IX")
unittest.main(argv=[''], exit=False, verbosity=2)
Вывод: (Red — тесты падают, это ожидаемо; устойчивая концовка)
... FAIL ... FAIL ... FAIL ---------------------------------------------------------------------- Ran 3 tests in 0.001s FAILED (failures=3)
Три падения — цель поставлена. Теперь шаг Green: пишем минимальную реализацию, которая делает тесты зелёными.
import unittest
def roman(n):
table = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
]
result = ""
for value, symbol in table:
while n >= value:
result += symbol
n -= value
return result
class TestRoman(unittest.TestCase):
def test_one(self):
self.assertEqual(roman(1), "I")
def test_four(self):
self.assertEqual(roman(4), "IV")
def test_nine(self):
self.assertEqual(roman(9), "IX")
def test_complex(self):
self.assertEqual(roman(58), "LVIII") # 50 + 5 + 1 + 1 + 1
def test_big(self):
self.assertEqual(roman(1994), "MCMXCIV")
unittest.main(argv=[''], exit=False, verbosity=2)
Вывод: (Green — все тесты проходят)
test_big (__main__.TestRoman.test_big) ... ok test_complex (__main__.TestRoman.test_complex) ... ok test_four (__main__.TestRoman.test_four) ... ok test_nine (__main__.TestRoman.test_nine) ... ok test_one (__main__.TestRoman.test_one) ... ok ---------------------------------------------------------------------- Ran 5 tests in 0.000s OK
Шаг Refactor: код уже компактен, но мы могли бы, например, вынести таблицу в константу модуля. И вот ключевое: пока тесты зелёные, любой такой рефакторинг безопасен — сломаем поведение, тесты тут же покраснеют.
Ритм TDD
| Шаг | Что делаем | Состояние тестов |
| Red | пишем тест на новое поведение | красные (падают) |
| Green | минимальный код «лишь бы прошло» | зелёные |
| Refactor | улучшаем код, не меняя поведения | остаются зелёными |
Почему «минимальный» код на шаге Green
Новичков смущает совет писать минимальный код, лишь бы тест прошёл, — кажется, что это халтура. Но в этом и смысл TDD: код двигают вперёд только тесты. Каждый новый тест задаёт следующее требование, и вы реализуете ровно его, не больше. Это защищает от двух бед: от лишнего кода «на всякий случай», который никто не проверяет, и от ситуации, когда вы написали много, а часть осталась без тестов. Маленькие шаги Red-Green держат код и тесты всегда в синхроне.
TDD как способ проектирования
Главная ценность TDD — не столько тесты, сколько то, что вы сначала думаете о поведении, а уже потом о реализации. Записывая тест, вы фактически проектируете интерфейс функции: как её зовут, что принимает, что возвращает, как ведёт себя на краях. Часто на этом этапе обнаруживаешь, что исходная идея неудобна в использовании, — и правишь дизайн ещё до написания кода. Получается, тесты улучшают не только надёжность, но и сам API. А накопленный набор тестов потом бесплатно защищает от регрессий.
Итог
- TDD: сначала падающий тест (Red), потом минимальный код (Green), потом чистка (Refactor).
- Тест-первый заставляет думать о поведении и гарантирует покрытие.
- Увиденное «красное» подтверждает, что тест действительно что-то проверяет.
- Зелёные тесты делают рефакторинг безопасным.