Инженерная практика
Когда TDD окупается, а когда активно вредит
Команду просят за два дня интегрировать нового вендора рекомендаций, чтобы решить, стоит ли подписывать контракт. Сеньор настаивает на полном TDD. Они тратят день на написание тестов против API, форму ответа которого они всё ещё угадывают, мокая эндпоинты, которых пока не понимают. На второй день реальные ответы вендора вообще не совпадают с замоканными — постраничные, иначе вложенные, с лимитами запросов, о которых документация умолчала. Каждый тест переписывается или удаляется. «Дисциплина» стоила им дня на фиксацию дизайна, на который у них не было права коммититься, на коде, чьё единственное назначение — быть выброшенным после решения. TDD не провалился; его применили к единственной ситуации, которую он карает.
Цена реальна и смещена вперёд
Сначала будем честны о цене. Контролируемые замеры дают тест-сначала примерно на 15–22% больше времени на входе, чем написание той же фичи тест-потом, и разрыв между исследованиями сам по себе и есть урок: академические эксперименты на хорошо специфицированных задачах склонны показывать выигрыш от TDD, тогда как несколько индустриальных отчётов о реальной, грязной работе показывают потерю продуктивности. Переменная, объясняющая раскол, — это насколько хорошо вы знали ответ до того, как начали. TDD смещает вперёд цену фиксации поведения. Когда поведение познаваемо и код проживёт годы, это смещение вперёд — инвестиция: обратная связь по дизайну сейчас, регрессионная сеть навсегда. Когда поведение неизвестно, а код короткоживущий, то же смещение вперёд — чистая трата: вы платите за спецификацию того, о чём вот-вот узнаете, что оно неверно.
Так что отдача TDD управляется двумя переменными, а не одной. Стабильность спецификации: можете ли вы заявить корректное поведение до написания кода? Время жизни кода: будут ли этот код читать и менять месяцами, или его удалят на следующей неделе? Высоко по обоим — биллинговый движок, флоу аутентификации, долгоживущее доменное ядро — и отдача TDD на пике. Низко по обоим — спайк, одноразовый скрипт миграции, UI, который вы дважды переделаете до запуска — и дисциплина берёт с вас плату за уверенность, которой у вас нет, на коде, который не выживет.
| Ситуация с кодом | Спека стабильна? | Долгоживущий? | Вердикт по TDD |
|---|---|---|---|
| Биллинг / аутентификация / доменное ядро | Да | Да | Полный TDD — пик отдачи |
| Исследовательский спайк / проба вендора | Нет | Нет (удалить после) | Без тестов — спайк, потом переписать |
| Быстро меняющийся UI / вёрстка | Нет (дизайн в движении) | Возможно | Тестируйте логику, не пиксели; отложите UI-тесты |
| Одноразовый скрипт миграции | Возможно | Нет | Тестируйте только опасные части |
Спайк, потом переписать — не делайте TDD неизвестного
Правильный инструмент для неизведанной территории — это спайк: быстрая, одноразовая программа, написанная без тестов, чья единственная задача — ответить на вопрос: работает ли этот вендор, достаточно ли быстр этот алгоритм, какой формы эти данные. Вы оптимизируете под скорость обучения, а не под корректность, и вы выбрасываете её. Затем, как только спайк превратил неизвестное в известное, вы переписываете боевую версию с TDD — теперь, когда вы действительно можете заявить спеку. Попытка сделать TDD спайка инвертирует порядок: вы специфицируете до того, как поняли, и платите налог 15–22% за фиксацию догадок, которые вот-вот выбросите. Сеньорский режим провала здесь — неуместная строгость: применение дисциплины жёстче всего ровно там, где отсутствует её предусловие (познаваемая спека).
UI — другая классическая ловушка, и стоит быть точным, а не догматичным. Проблема не в том, что «UI нельзя тестировать», — а в том, что быстро меняющаяся презентация меняется ежедневно, тогда как логика стабильна. Тестируйте логику (редьюсер, форматтер, валидацию, конечный автомат) жёстко; откладывайте или утончайте тесты, которые утверждают точную разметку и пиксели, пока дизайн меняется, потому что они ломаются на каждом редизайне и не защищают ничего долговечного. Зафиксируйте UI тестами, когда он стабилизируется, а не пока его ещё спорят в Figma.
Test-induced design damage — реальный провал, а не отговорка
Аргумент DHH 2014 года — «test-induced design damage» (вред дизайну, нанесённый ради тестируемости) — называет настоящий режим провала, который надо принимать всерьёз, даже если вы практикуете TDD. Это когда стремление сделать код юнит-тестируемым толкает вас добавить опосредование, служащее тесту, а не пользователю: выделение слоя, введение интерфейса и мока, расщепление цельной вещи на куски только ради того, чтобы тест мог её изолировать. Результат — больше абстракции, больше файлов, больше церемонии и дизайн, который хуже для всех, кроме теста. Честная позиция из разговоров «Is TDD Dead?» — это синтез: давление TDD на дизайн ценно, когда оно вскрывает реальную проблему связанности (как god-объект из урока 01), и вредно, когда вы корёжите дизайн ради удовлетворения правила тестируемости как такового. Навык — это умение их различать: слушайте тест, когда он вскрывает запах дизайна, игнорируйте его, когда он требует церемонии.
Почему это работает
Глубинная причина, почему «всегда TDD» и «TDD мёртв» оба неверны, в том, что TDD — это ставка на определённость, а определённость не однородна по вашей кодовой базе. Там, где вы знаете спеку и код проживёт годы, заплатить наперёд за фиксацию поведения и оставить регрессионную сеть — отличный размен. Там, где вы не знаете спеку, заплатить наперёд за спецификацию догадок — это отрицательная ценность, и спайк существует именно затем, чтобы купить ту определённость, которую TDD предполагает у вас уже имеющейся. Сеньорский навык — не фиксированный ответ; это считывание того, в каком режиме вы находитесь — стабильный-и-долговечный против неизвестного-и-одноразового — и переключение инструментов на границе вместо применения одного ритуала к обоим.
У вас два дня, чтобы оценить незнакомый API вендора и решить, подписывать ли. Как вы подойдёте к коду?
Какие две переменные сильнее всего определяют, окупится ли TDD на данном куске кода?
Что именно есть 'test-induced design damage' и чем оно отличается от легитимного давления TDD на дизайн?
Упорядочьте сеньорский воркфлоу спайк-потом-переписать для неизведанной территории:
- 1 Признать, что спека неизвестна, а код одноразовый
- 2 Написать быстрый спайк без тестов, оптимизируя под скорость обучения
- 3 Использовать спайк, чтобы превратить неизвестное в познаваемую спеку
- 4 Выбросить спайк
- 5 Переписать боевую версию с полным TDD теперь, когда спека заявлена
- 01Сеньор настаивает на полном TDD для двухдневной оценки вендора. Почему это неверный выбор и какой верный?
- 02Примирите «давление TDD на дизайн ценно» (урок 01) с «test-induced design damage» DHH. Как их различить?
TDD — это ставка на определённость, а определённость не однородна по кодовой базе, поэтому и «всегда TDD», и «TDD мёртв» оба неверны. Цена реальна и смещена вперёд — примерно на 15–22% больше времени на входе, чем тест-потом — и раскол между академическими выигрышами и индустриальными потерями объясняется одним: насколько хорошо вы знали ответ до начала. Отдачей управляют две переменные: стабильность спеки (можете ли вы заявить корректное поведение наперёд) и время жизни кода (месяцы против удаления на следующей неделе). Высоко по обоим — биллинг, аутентификация, долгоживущее доменное ядро — это пик отдачи: обратная связь по дизайну сейчас, регрессионная сеть навсегда. Низко по обоим — исследовательский спайк, проба вендора, одноразовый скрипт — и вы платите за спецификацию догадок об одноразовом коде, поэтому правильный инструмент — спайк: быстрая, без тестов, одноразовая программа, превращающая неизвестное в познаваемую спеку, после чего вы переписываете боевую версию с TDD. UI — та же логика: тестируйте стабильную логику жёстко, откладывайте тесты на пиксели-и-разметку, пока дизайн меняется. А test-induced design damage DHH — это настоящий режим провала, который надо держать рядом с давлением на дизайн из урока 01: опосредование, служащее тесту, а не пользователю, — это вред, тогда как тест, вскрывающий реальный запах связанности, — это давление, которое стоит послушать; сеньорский навык — различать их на границе.