Посмотрите, я хочу сказать кое-что, что может вызвать неодобрительные взгляды на вашем следующем командном стендапе: Разработка через тестирование (TDD) не всегда является решением, и притворство в обратном может стоить вам дороже, чем вы сэкономите. Прежде чем закрыть эту вкладку и написать гневный комментарий, выслушайте меня. Я не говорю, что TDD плоха. Я говорю, что это инструмент, и, как и у любого инструмента, есть ситуации, в которых лучше использовать что-то другое. Использование молотка для всего не делает вас лучшим плотником — это делает вас плотником с множеством погнутых гвоздей.

Фаза медового месяца TDD, в которой мы все живём

В современной разработке программного обеспечения существует негласное предположение, что если вы не используете TDD, то вы, по сути, пишете код с закрытыми глазами. Дошло до того, что признать, что вы не используете TDD, — всё равно что признать, что вы не чистите зубы. Но вот в чём дело: обещание и реальность имеют удивительно большой разрыв между собой. Идея заманчива. Сначала пишите тесты. Смотрите, как они терпят неудачу. Пишите код. Смотрите, как они проходят. Рефакторите с уверенностью. Ваш код всегда работает. Ошибки выявляются немедленно. Вы спите, как младенец, зная, что ваша кодовая база неуязвима. Только… не всегда так.

Реальная стоимость: время, которое вы никогда не вернёте

Давайте поговорим о слоне в комнате: TDD делает вас медленнее, особенно в начале. Не в конечном итоге. Не в долгосрочной перспективе. Прямо сейчас. Немедленно. Когда вы новичок в TDD, происходит следующее: вы тратите 30 минут на написание теста, затем 10 минут на написание кода, чтобы он прошёл, затем ещё 20 минут на рефакторинг. Ваш коллега пишет ту же функцию за 15 минут. Конечно, у него есть три ошибки, но они проявятся только в продакшене — а это проблема кого-то другого, верно? Только этим «кем-то другим» часто оказываетесь вы.

Отчаяние в «аду тестов»

Есть особый вид страданий, который я называю «ад тестов», и я готов поспорить, что вы его испытали: вы пишете функции, все тесты проходят, но когда вы рефакторите — что-то всегда ломается. Проблема обычно заключается в следующем: ваши тесты слишком тесно связаны с деталями реализации. Вы тестируете «как», а не «что». Так что, когда вам неизбежно нужно изменить «как» (потому что, знаете ли, вы что-то узнали), тесты взрываются.

Когда TDD просто… не подходит

Не весь код одинаков, и не весь код хочет тестироваться способом TDD. Попробуйте написать TDD для:

  • Загрузок и скачиваний файлов — вы имитируете файловую систему. Вы делаете вид, что читаете файлы. Вы, по сути, тестируете свои тестовые заглушки, а не фактическое поведение.
  • Реального времени WebSocket коммуникации — тестирование изменений состояния при отключении, переподключении и частичной доставке сообщений? Удачи. Вы можете смоделировать это, конечно, но тогда вы тестируете макеты, а не реальность.
  • На основе браузера UI код — хотите проверить, что нажатие кнопки вызывает модальное окно, которое скользит слева? Отлично, теперь вы пишете UI тесты с огромным накладным расходом на настройку и хрупкими селекторами. Это модальное окно может работать отлично в продакшене, но сбоить в вашем тесте из-за проблемы с CSS таймингом.
  • Интеграций с внешними API — вы имитируете API? Тогда вы тестируете макеты. Вы бьёте реальное API? Тогда ваши тесты медленные, нестабильные и зависят от внешних сервисов.
  • Встроенных систем и взаимодействий с аппаратурой — получайте удовольствие от тестирования того, что включает реле. Я подожду. Для этих сценариев вы можете навязать TDD. Вы можете написать тесты. Но вы будете бороться с методологией всё время, писать больше тестового кода, чем продуктового, и создавать ложное доверие, потому что ваши тесты проходят, но ваша фактическая функция сломана. Иногда лучше использовать интеграционные тесты, ручное тестирование, тестирование на основе свойств или старые добрые исследовательские тесты. Инструмент должен соответствовать проблеме, а не наоборот.

Ловушка устаревшего кода

Вот о чём евангелисты TDD говорят не так уж много: TDD намного проще, когда вы начинаете с чистого листа. Это совершенно другая история, когда вы работаете с устаревшим кодом, который был создан без какой-либо мысли о тестируемости. Я однажды присоединился к команде, которой было поручено добавить функции в кодовую базу возрастом 15 лет, где один метод управлял подключениями к базе данных, бизнес-логикой и рендерингом UI в одной прекрасной функции на 2000 строк. Сказать мне писать сначала тесты было всё равно что сказать мне переорганизовать всю кухню перед приготовлением завтрака. Чтобы на самом деле применить TDD к этому коду, вам нужно:

  1. Рефакторить монолитные функции в тестируемые части.
  2. Извлечь зависимости и внедрить их.
  3. Удалить жёстко закодированные подключения к базе данных.
  4. Разделить проблемы, которые никогда не предполагалось разделять. Только затем вы сможете писать тесты в стиле TDD. Но кто платит за этот рефакторинг? Клиент? Ему нужны функции. Бизнес? Ему нужна рентабельность инвестиций. Вы? Вы уже отстаёте от графика.

Предательство дизайна

Вот что тонкое, о чём сторонники TDD не рекламируют достаточно: TDD может фактически подталкивать вас к худшему дизайну на уровне системы. Когда вы сосредоточены на написании тестов для отдельных единиц, вы естественным образом пишете код, который тестируем на уровне единиц. Тестируемый код имеет меньше зависимостей, более модульный и имеет узкий scope. Всё хорошо, правда? Только когда вы отстраняетесь и смотрите на всю систему, вы понимаете, что создали нечто, что:

  • Перепроектировано с внедрением зависимостей повсюду.
  • Раздроблено на столько мелких частей, что реальная архитектура невидима.
  • Оптимизировано для юнит-тестирования, но ужасно для производительности.
  • Полное интерфейсов и абстракций, которые никто не может мысленно смоделировать. Это называется «потеря леса за деревьями». Вы стали так хороши в тестировании мелочей, что забыли думать о больших вещах. Реальные системы нуждаются в:
  • Ясных архитектурных паттернах.
  • Разумных границах между доменами.
  • Проектировании, ориентированном на производительность.
  • Понятном потоке данных. TDD не помогает этим по своей сути. Иногда это даже вредит.

Когда клиенты (правомерно) не заботятся

Давайте на мгновение станем неудобными: ваш клиент платит вам не за тесты. Они платят вам за функции. Я знаю, я знаю. Тесты предотвращают ошибки. Тесты экономят деньги в долгосрочной перспективе. Тесты позволяют рефакторить с уверенностью. Всё верно. Но с точки зрения клиента (особенно стартапа или организации с ограниченными ресурсами), это звучит так: «Дайте мне больше времени, чтобы написать код, который вы не увидите, функции, которые вы не будете использовать, и надейтесь, что это как-то улучшит ситуацию позже». Если клиент на ограниченном бюджете и жёстких сроках, и вы говорите им, что пишете сначала тесты, вот что они слышат: «Я могу запустить ваш MVP за три недели, или я могу запустить его за шесть недель, но второй вариант имеет невидимые достоинства». Теперь, если вы строите критически важную систему в Google, это невидимое достоинство стоит миллиарды. Но если вы строите лендинг, который должен проверить идею до того, как закончится финансирование, невидимое достоинство — это роскошь. Умные разработчики учатся быть прагматичными. Вам не нужен полный TDD для всего. Вам нужны:

  • Тесты для критического пути (части, которая, если сломана, рушит бизнес).
  • Тесты для сложной логики, где ошибки дороги.
  • Не тесты для всего. Этот прагматизм — не слабость. Это признак того, кто понимает, что методология служит проекту, а не наоборот.

Кошмар рефакторинга, о котором никто не говорит

Вот сценарий, который будет преследовать ваши сны: вы строили функции с TDD в течение восемнадцати месяцев. Ваш код хорошо протестирован. Ваши тесты проходят. Всё отлично. Затем на совещании по планированию вы понимаете, что весь ваш архитектурный подход ошибочен. Может быть, вы выбрали не ту базу данных. Может быть, ваш дизайн API был ошибочным. Может быть, изменились требования к производительности. Может быть, вы узнали что-то фундаментальное о предметной области. Теперь вам нужно рефакторить. Не просто улучшить. Рефакторить. Большие изменения. С TDD это становится кошмаром: когда вы меняете реализацию, все ваши тесты тоже меняются.

Ловушка умственной нагрузки

Написание тестов требует умственных усилий. Так же как и написание кода. TDD просит вас делать и то, и другое одновременно для каждой функции. В психологии есть концепция, называемая «когнитивная нагрузка» — количество умственных усилий, необходимых для выполнения задачи. TDD резко увеличивает когнитивную нагрузку. Вам нужно:

  • Понимать функцию.
  • Представлять все крайние случаи.
  • Писать тесты, которые охватывают эти крайние случаи.
  • Писать код, который заставляет тесты проходить.
  • Рефакторить, не ломая ничего. Вы