TDD: red-green-refactor

Перевернём привычный порядок: сначала тест, потом код — и пройдём полный цикл на живом примере.

TDD (Test-Driven Development) — практика, при которой сначала пишут падающий тест, затем минимальный код, чтобы он прошёл, и только потом улучшают код: цикл red → green → refactor.

Три фазы цикла

  • Red. Пишем тест на ещё не существующее поведение. Он падает — это нормально и ожидаемо.
  • Green. Пишем самый простой код, чтобы тест стал зелёным. Красота пока не важна.
  • Refactor. Тест зелёный — теперь улучшаем код, не меняя поведения. Тест страхует: если что-то сломаем — он покраснеет.

Зачем «сначала тест»

Это меняет мышление. Сначала формулируешь, что функция должна делать (в виде проверок), и только потом — как. Тесты получаются по определению, код — минимальным и проверяемым, а «потом напишу тесты» (которое никогда не наступает) исключено.

Живой пример: функция fizzbuzz

Задача: для числа вернуть «Fizz» если делится на 3, «Buzz» если на 5, «FizzBuzz» если на оба, иначе — само число строкой. Пройдём цикл целиком. Сначала формулируем тесты (red), затем реализуем (green) — здесь покажем уже зелёное состояние с готовой реализацией.

# GREEN: минимальная реализация, проходящая тесты
def fizzbuzz(n):
    if n % 15 == 0:
        return "FizzBuzz"
    if n % 3 == 0:
        return "Fizz"
    if n % 5 == 0:
        return "Buzz"
    return str(n)


# Тесты, которые мы написали ПЕРВЫМИ (red), теперь зелёные
def test_fizzbuzz():
    assert fizzbuzz(1) == "1"
    assert fizzbuzz(3) == "Fizz"
    assert fizzbuzz(5) == "Buzz"
    assert fizzbuzz(15) == "FizzBuzz"
    assert fizzbuzz(30) == "FizzBuzz"

test_fizzbuzz()
print("GREEN: все тесты fizzbuzz проходят")

Вывод:

GREEN: все тесты fizzbuzz проходят

Рефакторинг под защитой тестов

Теперь можно безопасно переписать функцию красивее — например, через сборку строки — и тот же набор тестов мгновенно подтвердит, что поведение не изменилось.

def fizzbuzz(n):
    out = ""
    if n % 3 == 0:
        out += "Fizz"
    if n % 5 == 0:
        out += "Buzz"
    return out or str(n)


# REFACTOR: реализация другая, тесты — те же и всё ещё зелёные
assert fizzbuzz(1) == "1"
assert fizzbuzz(3) == "Fizz"
assert fizzbuzz(5) == "Buzz"
assert fizzbuzz(15) == "FizzBuzz"
print("REFACTOR: поведение сохранено, тесты подтверждают")

Вывод:

REFACTOR: поведение сохранено, тесты подтверждают

Две совершенно разные реализации fizzbuzz прошли один и тот же набор тестов. Именно это даёт TDD: смелость менять код, потому что тесты ловят регрессии мгновенно.

Итог

  • TDD: red (падающий тест) → green (минимальный код) → refactor (улучшение).
  • Сначала формулируем «что», потом «как» — тесты получаются по определению.
  • Тесты позволяют безопасно рефакторить: поведение зафиксировано.
Проверьте себя
1. В каком порядке идут фазы TDD?
Agreen → red → refactor
Bred → green → refactor
Crefactor → red → green
Dcode → test → deploy
2. Почему в TDD тест пишут ДО кода?
AЧтобы код был длиннее
BЧтобы сначала зафиксировать «что» должна делать функция, и тесты появились по определению
CТак быстрее печатать
DЭто требование компилятора
3. Что даёт фаза refactor под защитой тестов?
AВозможность сломать поведение незаметно
BВозможность безопасно улучшать код: если поведение изменится, тест покраснеет
CУдаление всех тестов
DУскорение компиляции
Поддержать проект