Цикл «красный — зелёный — рефакторинг»

Что это такое

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

Почему это важно

У каждого шага своя ошибка, которую он не даёт совершить. Красный доказывает, что тест связан с кодом и действительно способен упасть: тест, который проходит впустую, надолго даёт ложную уверенность. Зелёный заставляет написать самую маленькую реализацию и тем самым не пускает догадки в проектирование. Рефакторинг поддерживает проект в форме, пока тесты зелёные, — поэтому улучшения идут постоянно, а не в рискованный сеанс через месяцы. Если выполнять все три шага, получится код, каждую строку которого потребовал тест, и тесты, провал которых видели.

Ключевые идеи

Сначала красный, и посмотрите на провал

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

тест: expect(discountFor(customerWith(years: 5))).toBe(0.05)
прогон -> провал: discountFor is not defined          (ожидаемо)

Зелёный: простейшее, что проходит

Кода должно быть ровно столько, чтобы тест прошёл, даже если это выглядит подлогом: жёстко заданное значение — законный зелёный, когда тесту нужен всего один случай. Обобщение вынудит очередной тест, и к тому моменту его форма будет уже известна, а не угадана; шаг намеренно мал, ведь чем он меньше, тем вернее, что ничего другого он не сломал.

function discountFor(customer) {
  return 0.05;   // зелёный для одного случая; следующий тест требует правила
}

Рефакторинг под зелёным

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

красный -> зелёный -> рефакторинг (тесты остаются зелёными) -> красный ...

Держите цикл маленьким

Цикл на час — не цикл, а функция вместе с тестом. Цель — одно поведение за круг: одна ветка, одно правило, один граничный случай. Тогда провал теста однозначен, реализация тривиальна, а проектирование складывается из последовательности решений, а не из одной крупной догадки.

Пусть тесты вытягивают проектирование

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

Практика

Проведите одно правило через три цикла

Возьмите небольшое правило — скидку на цену, проверку данных, смену состояния. Напишите тест для самого простого случая и добейтесь зелёного. Затем добавьте второй тест, который заставит правило обобщиться, а после него третий — на граничный случай.

Готово, когда: каждый из трёх тестов сначала видели упавшим, а в итоговой реализации нет ветки, которой не требует ни один тест.

Восстановите пропущенный красный

Возьмите тест, написанный уже после кода, и нарочно сломайте реализацию — как можно незаметнее. Прогоните набор.

Готово, когда: провал теста увидели, а если тест не упал, значит, вы нашли случай, которого ему не хватало.

Частые ошибки

  • Тест после кода, названный TDD — выгода для проектирования потеряна, а сам тест, возможно, вообще не умеет проваливаться; исправить: напишите тест, прогоните его, увидьте провал и только потом реализуйте.
  • Реализация большего, чем требует тест — непокрытые ветки и спекулятивная общность копятся; исправить: пишите простейший проходящий код и позвольте следующему тесту вынудить обобщение.
  • Рефакторинг при красном тесте — в работе два изменения сразу, и провал ни к чему не отнести; исправить: сначала вернитесь к зелёному.
  • Цикл настолько крупный, что это уже функция — провал теста неоднозначен, а проектирование — одна большая догадка; исправить: одно поведение на цикл.
  • Пропуск цикла для кода, который кажется тривиальным — у тривиального кода объявится крайний случай, которого вы не учли; исправить: применяйте цикл для логики и честно признавайте, где он не подходит.

Что читать дальше