Во вторник на планёрке техлид объявил новый порядок: с этого спринта каждая задача открывается тестом, без исключений. Через три недели скорость спринта просела, дизайнер ждал прототип, а разработчики подыскивали отговорки. Порядок откатили, и команда потеряла не подход, а его границы. Разработка через тестирование, она же TDD, хромает ровно там, где её натягивают на неподходящую работу, и расцветает там, где требование известно до первой строки кода.
Здесь нет выбора между верой и скепсисом, есть выбор инструмента под форму работы. Ниже разбор механики цикла, зон окупаемости, знаменитых провалов и практики внедрения, какой она получается у работающих команд.
Цикл красный зелёный рефактор как рабочая дисциплина
Методику собрал Кент Бек в конце 1990-х внутри Extreme Programming, а сжатое описание Фаулера укладывается в три шага, повторяющиеся десятки раз за день: составить список проверок, взять один случай, довести его через красный прогон и зелёный минимум кода до рефакторинга. Цикл меряется минутами, а не часами, и это не педантизм: короткий шаг сохраняет главный навигационный эффект, когда проверка подсказывает интерфейс раньше реализации. Писать проверку первой полезно и по другой причине. Автор думает о том, как код будут звать, а не о том, как он устроен изнутри, и граница модуля выходит чище.
Разберём ритм на задаче разбора строки вида 1h30m в секунды. Сначала два случая:
def test_parses_hours_and_minutes():
assert parse_duration("1h30m") == 5400
def test_parses_days():
assert parse_duration("2d") == 172800
Проверка красная, функции ещё нет. Первая зелёная версия может быть наглой:
def parse_duration(spec):
return 5400
Обманка здесь не шутка, а приём из практики Бека: она закрывает текущий случай и заставляет следующий уточнить требование. Третий случай работает как тройная точка: два значения не отличают обманку от честной логики, три вынуждают обобщать.
def test_parses_combined_units():
assert parse_duration("2d4h") == 187200
Дни падают, обманка сдаётся, рождается обобщение:
import re
UNITS = {"m": 60, "h": 3600, "d": 86400}
def parse_duration(spec):
return sum(int(n) * UNITS[u] for n, u in re.findall(r"(\d+)([mhd])", spec))
Фаулер называет самым частым провалом цикла пропущенный рефакторинг: без него накапливается свалка фрагментов, пусть и прикрытая проверками. Ещё одна забытая деталь, список тестов до кода. Его чертят на планёрке, вычёркивают по мере готовности и пополняют по ходу дела, и именно он превращает разрозненные проверки в карту требований. Порядок тоже инженерное решение: Фаулер советует брать случаи, которые быстрее выводят к узловым точкам дизайна, а периферию откладывать. Ритм в 5-15 минут на шаг держит концентрацию и защищает от вечернего марафона под названием доделаю и проверю потом.
Задачи с ясной спецификацией раскрывают силу подхода
Сила цикла видна там, где требование формулируется до кода: расчёты, парсеры, валидаторы, автоматные переходы. Формула тарифа известна из постановки, вопрос лишь в аккуратности реализации, и проверка впереди работает как смета. Отдельный выигрыш приносит работа с дефектами. Тикет сообщает, что перевод уходит с пустого счёта. Первый шаг выглядит так:
def test_transfer_rejects_zero_balance():
with pytest.raises(InsufficientFunds):
transfer(source=Account(balance=0), target=Account(), amount=500)
Проверка падает, инцидент воспроизведён на локальной машине, затем идёт правка кода, набор зеленеет, а строка остаётся в проекте навсегда. Через полгода историю забудут все, но охрана границы продолжит работать. Дешёвого способа закрепить исправление надёжнее до сих пор не придумали.
Унаследованный код заслуживает отдельного слова. Проверки впереди там почти невозможны, потому что требование уже закодировано в старой логике, иногда вместе с сюрпризами, которые никто не решается трогать. Майкл Фезерс в книге 2004 года о работе с унаследованным кодом описал характеризующие проверки: они фиксируют поведение как есть, сюрпризы включительно, и только после этого код разрешается менять. Это другое чтение цикла, не тест задаёт поведение, а тест страхует вмешательство.
Перечень задач, где подход стабильно окупается, короткий:
1. расчётная логика с формулами и границами, тарифы, налоги, лимиты и округления;
2. парсеры, сериализация, конвертеры форматов и валидация входа;
3. конечные автоматы, статусы заказов, матрицы переходов прав доступа;
4. дефекты из трекера, когда тест воспроизводит инцидент раньше исправления;
5. библиотечные модули, которые соседние команды подключают как зависимость.
Общее у всех пунктов одно: ответ известен до написания кода, спор идёт только о тщательности реализации. Здесь цикл экономит часы отладки и снижает нервозность ревью, потому что проверка читается как спецификация.
Сценарий из практики расчётного модуля показывает разницу наглядно. Аналитик приносит таблицу тарифов, разработчик переносит каждую строку таблицы в отдельную проверку, и через час набор становится исполняемой копией постановки. Правка тарифа через полгода открывает нужный файл за минуту: требование записано в исполняемом виде, а не в треде переписки, где половина сообщений устарела ещё до релиза.
Прототипы интеграции и исследовательский код ломают ритм
Контрпример строится вокруг неопределённости. Экран согласования документа: дизайнер показывает кликабельный макет, продукт уточняет сценарий по ходу показа, кнопка перезжает в другой блок уже завтра. Тест на такое требование пишут трижды и трижды выбрасывают, а дисциплина превращается в имитацию. Визуальная работа вообще живёт по другой петле: глаз ловит то, что формулами описать трудно, и мгновенная обратная связь от макета ценнее любой проверки.
Хуже всего проверки впереди ведут себя при интеграции с чужим сервисом без документации. Тест фиксирует догадку об ответе, догадка не совпадает с реальностью, и набор врёт с уверенным зелёным светом. Исследовательские ветки, спайки, подбор параметров, разбор формата без спецификации живут в отдельном режиме: код выбрасывают или переписывают целиком, и экономить тестами чужие эксперименты значит оплачивать двойную работу. Зрелые команды помечают такие ветки в трекере и не требуют от них привычной дисциплины, и это не поблажка, а честная смета.
Одноразовые скрипты миграции и разовые выгрузки живут по тем же правилам исследовательского режима: строка исполнится один раз, ошибки проявятся на реальном объёме, а половина проверок таких скриптов описывает воображаемый мир. Аналитика с подбором параметров ещё жёстче: критерий успеха формулируется после просмотра результатов, и проверке впереди просто нечего утверждать.
Спор 2014 года о вреде тест-индуцированного дизайна
Весной 2014 года Дэвид Хайнемайер Ханссон, автор Rails, выпустил текст с громким тезисом: TDD умерла, да здравствует тестирование. Его главный аргумент получил имя test-induced design damage, вред дизайну от самих тестов. Погоня за мгновенно проверяемым кодом плодит лишние слои и изоляцию коллабораторов, архитектура гнётся под проверки, а читабельность платит за это первой. Ханссон показывал, как собственный код Rails оброс слоями ради проверяемости.
Кент Бек и Фаулер ответили серией публичных разборов, и полезное зерно компромисса свелось к двум вещам. Дисциплина быстрых проверок остаётся ценной, догма о проверке каждой строчки без разбора вредна. Интеграционные проверки после той дискуссии получили заслуженную реабилитацию, а требование гонять цикл на каждой задаче подряд ушло в прошлое: команды получили право выбирать глубину по типу работы. Спор интересен и тем, что обе стороны спорили не о тестах, а о том, кто хозяин дизайна: код ради тестов или тесты ради кода.
Дискуссия вылилась в серию публичных онлайн-встреч трёх авторов в мае 2014 года, и записи тех разговоров до сих пор всплывают на собеседованиях. Показательная деталь: Ханссон ни в одной из встреч не отказывался от тестов, его тезис всегда был о пропорциях и о праве команды выбирать глубину. Бек в ответ называл цикл зеркалом дисциплины, а не её источником: проверка впереди не спасает хаотичное мышление, но даёт упорядоченному мышлению поручни.
Эмпирика исследований о цене и выгоде подхода
Классическая работа 2008 года по четырём промышленным командам в Microsoft и IBM фиксировала падение плотности дефектов в диапазоне 40-90 процентов ценой роста времени разработки на 15-35 процентов. Авторы честно предупреждали: выборка маленькая, продукты небольшие, переносить цифры на любой проект наивно. Ценность работы не в точных процентах, а в форме кривой: качество растёт нелинейно к затратам, и на задачах с дорогими ошибками ставка оправдана.
Контролируемый эксперимент 2004 года в лабораторных условиях дал ту же форму результата: пары с проверками впереди пропускали меньше дефектов и тратили больше времени. Сюрприз принесли команды, применявшие цикл на знакомом домене: разница в скорости почти исчезала, оставалась разница в надёжности. Это соответствует интуиции, потому что основной налог цикла платится на этапе осмысления требований, а не на этапе набора строк.
Дешевле собственная телеметрия. Доля дефектов, прилетевших в релиз мимо набора, время от открытия тикета до зелёного прогона, стоимость возврата правки, вот три числа, по которым видно, работает ли цикл. Замечу и про объём: параллельная жизнь тестов удваивает массу кода в репозитории, поэтому ревью смотрит и на проверки, иначе через два года раскладка превращается в архив туманных имён. Счёт тестов метрикой не служит, им слишком легко играть. Расход, о котором забывают, ложные срабатывания. Набор, краснеющий от случайных сетевых тайм-аутов, приучает команду игнорировать красный свет, и сигнал обесценивается быстрее, чем успевает окупиться любая дисциплина.
Внедрение в команде от пилотного модуля до правила готовности
Мандат сверху на все задачи подряд даёт обратный эффект, вступительная сцена это показала. Рабочий путь начинается с пилота: новый модуль или сервис с нуля, где спецификация свежая и цена ошибки понятна. Дефектные ветки получают жёсткое правило, сначала тест, воспроизводящий инцидент, потом правка, и через квартал этот рефлекс уже не надо поддерживать приказами. Прототипы и интеграции помечают в трекере как зону без проверок впереди, и никто не выглядит отступником.
Правило готовности в Definition of Done дополняют строкой о проверках, а парное программирование сглаживает порог входа: опытный участник держит направление, новичок учится формулировать требование в виде красной проверки. Метрики заменяют лозунги: прорвавшиеся в релиз дефекты, время закрытия тикета, доля проверок, краснеющих при чужих правках. Наконец, у зоны действия методики есть границы по уровню владения инструментом: на младших ставках цикл сначала замедляет работу, и это нормально, покупается навык, а не скорость.
Первые две недели пилота планируют как эксперимент с критериями. Пары по часу, разбор требований на входе, ретроспектива в пятницу, и три числа для сравнения: время закрытия задачи, дефекты, вернувшиеся из релиза, настроение участников по пятибалльной шкале. Честно закрытый пилот заменяет спор о методике разговором о цифрах, а спор о цифрах дешевле спора о вкусах.
Сопротивление снимают не приказом, а границами. Разработчик, чья ветка помечена как исследовательская, не объясняет отсутствие проверок впереди, зато ветка с расчётами проходит через этот ритм без обсуждений. Честная маркировка дешевле тотального контроля, и именно она отличает договор от диктата.
Границы применимости как признак зрелой команды
Цикл с проверками впереди похож на контракт, подписанный до начала работ: он дисциплинирует там, где стороны знают условия, и мешает там, где условия ещё ищут. Команда из вступительной сцены проиграла не из-за плохого метода, а из-за запрета на исключения, и второй попытки ей не дали именно потому, что границы так и не проговорили.
Хорошая новость в том, что раскладить границы легко. Расчёты, парсеры и автоматные переходы получают красно-зелёный ритм, прототипы и интеграции получают свободу, дефекты получают тест раньше правки. Такой расклад стоит дороже любого лозунга и дороже отговорки слишком заняты, чтобы писать тесты. Он и отличает команду, которая владеет инструментом, от команды, которую инструмент носит по коридорам спринта.