Это имеет смысл. Если я вынужден писать модульные тесты, я пишу в два раза больше кода, чем если бы я просто написал то, с чего хотел начать. Когда я вношу изменения в код, я также вынужден менять модульные тесты. Большая часть моего кода просто не поддается тестированию. Рано или поздно они устареют, и я начну игнорировать тесты. Поэтому они были огромной тратой времени. У меня сжатые сроки, потому что руководство заставляет меня работать быстрее, поэтому отказ от модульных тестов — это простой выбор. И не говорите мне, насколько глуп TDD.

Те из вас, кто читал другие мои статьи, понимают, что это не то, что я думаю. Но, к сожалению, этого мнения придерживается довольно много людей в нашей отрасли. Они действительно считают, что модульное тестирование не нужно и просто замедляет процесс.

Более вдумчивые люди, придерживающиеся этой позиции, полагаются на идею о том, что более высокие уровни тестирования включают в себя то, что охватывают модульные тесты. Нет причин писать модульные тесты, пока мы проводим интеграционное, системное, приемочное тестирование, тестирование производительности и безопасности. Мы получим такое же качество в конце процесса, если будем делать все эти другие вещи, так зачем тратить время на написание модульных тестов?

Они полностью упускают суть. Мы пишем модульные тесты, чтобы работать быстрее.

Строительные блоки

Разработка программного обеспечения похожа на строительство из блоков. Мы берем большую сложную проблему и разбиваем ее на управляемые части. Затем мы строим эти части и собираем их в еще большие части. В конце концов мы собрали большое количество частей в работающую систему.

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

Цикл отладки кода

В разработке программного обеспечения есть поговорка, что первые 90% проекта занимают столько же времени, сколько и последние 10%. Это потому, что когда вы отлаживаете, у вас нет возможности контролировать, сколько работы осталось. Существует множество эвристик, связанных с отслеживанием количества ошибок, времени их устранения и т. д., чтобы попытаться предсказать, когда проект выйдет на стабильный релиз, но это всего лишь догадки. Иногда исправление этой последней ошибки может привести к поломке других вещей. Иногда эти регрессии происходят за кулисами, поэтому вы не знаете о них до следующего цикла принятия пользователем.

Вот как выглядит типичный проект разработки программного обеспечения, который не тратит время на модульное тестирование:

10% — требования/дизайн, 30% — кодирование, 60% — цикл тестирования/отладки. Большинство улучшений в разработке программного обеспечения сосредоточено на попытках сократить эти 30% времени, затрачиваемого на кодирование. Это логика, которая говорит, что если мы исключим время, затрачиваемое на модульное тестирование, мы получим код быстрее.

Разве не имеет смысла делать все возможное, чтобы вырвать кусок из цикла тестирования/отладки? Отдача от экономии в 2 раза больше, чем от немного более быстрого кодирования.

Вернемся к аналогии с блоками. Отладка становится очень сложной, когда вы не знаете, какие из ваших блоков работают правильно. Вы буквально воздействуете на хрупкий фундамент. Любой из этих блоков может рухнуть и сломать другие вещи. И наоборот, гораздо проще, если мы знаем, что каждый из отдельных компонентов работает так, как ожидалось, и мы просто отслеживаем проблемы с взаимодействиями.

Допустим, нам требуется на 50% больше времени для написания модульных тестов по мере написания кода. (Я не согласен с этой оценкой, я думаю, что на самом деле быстрее работать с использованием TDD, чем без него.) Предположим, проект составляет 1000 часов. Вместо 300 часов кодирования мы будем кодировать 450 часов. Наше время тестирования/отладки нужно сократить всего на 25%, чтобы компенсировать дополнительное время, затрачиваемое на написание тестов.

Экономия составляет более 25%. К настоящему времени, имея сплошные блоки, мы можем:

  • У нас никогда не будет регресса. Та же ошибка не появится снова, потому что она была обнаружена предыдущим тестом. Для каждой обнаруженной ошибки мы создаем тест, который проверяет, что она не возвращается.
  • У нас не будет ошибок, когда поведение блока не соответствует требованиям. У нас есть тесты для блоков, поэтому мы знаем, что они работают, как указано.
  • Нам не нужно беспокоиться о каскадах дефектов. Если мы вносим изменения в одном месте, мы можем запустить весь набор тестов и узнать, не сломали ли мы что-то в другом месте.
  • Нам не нужно бояться чистить код. Мы можем перемещать вещи без страха. Мы можем упростить блок и гарантировать, что поведение не изменилось.
  • Настоящая кропотливая отладка выполняется быстрее, когда вы делаете это с помощью модульного теста, а не пошагово через отладчик. Большая часть времени на отладку тратится просто на ручную настройку условий для тестов. Большинство ошибок, которые вы находите, на самом деле просто тесты, которые не учитывались.

По моему опыту, начиная с действительно надежного набора модульных тестов, вы избавляетесь от 75% времени, затрачиваемого на отладку. Наш 1000-часовой проект превратился в 450 часов кодирования и 150 часов отладки. Весь проект занял на 40% меньше времени. Да и качество намного выше, чем при ad hoc подходе.

Эти 450 часов на кодирование — это много, особенно если вы следуете TDD. На самом деле кодировать с TDD так же быстро, как и без него. Когда-нибудь замирали мозги, пытаясь решить проблему с кодированием? Это редко случается, когда вы просто пытаетесь написать очередной неудачный тест или пишете достаточно кода, чтобы пройти тест. Каждый раз, когда вы выполняете тесты и получаете зеленый свет, ваш мозг получает выброс дофамина. Это поможет вам войти в поток и оставаться в нем. Реальный ответ заключается в том, что общая экономия приближается к 50–60%.

Когда я вношу изменения в код, я также вынужден менять модульные тесты.

Вы не правильно тестируете. Модульный тест должен проверять поведение. Это не должно быть связано с кодом. Единственный раз, когда вам нужно изменить тесты, должно быть, если вы изменяете ожидаемое поведение или добавляете дополнительные ожидаемые поведения.

Большая часть моего кода просто не поддается тестированию

Этого не может произойти, если вы сначала пишете свои тесты. Одним из основных преимуществ подхода, основанного на тестировании, является то, что он заставляет вас проектировать свой код таким образом, чтобы его можно было тестировать. Вы также обнаружите, что код, предназначенный для тестирования, намного менее хрупок.

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

Тесты устаревают

Нет, если вы тестируете поведение. Тесты на самом деле являются отличным способом документирования ожидаемого поведения. Тесты не могут устареть, если вы обновляете их по мере изменения ожидаемого поведения.

Руководство давит на меня, чтобы я ехал быстрее

Я сомневаюсь в этом. Руководство давит на вас, чтобы вы выпустили быстрее. Они хотят, чтобы вы были более продуктивными. Им все равно, как быстро вы печатаете или сколько кода пишете, они хотят видеть результаты. Потратив немного больше времени на кодирование, вы будете выглядеть героем, когда он пройдет приемочное тестирование.

Если хочешь идти быстро, иди медленно

Как говорят военные спецназовцы, «медленно плавно, плавно быстро».

Как сказано в заголовке этой статьи, модульные тесты меня замедляют. И это то, что заставляет меня двигаться быстрее.