TDD: Что пошло не так?
Эта статья является переводом материала «TDD: What went wrong or did it?».
В сфере разработки программного обеспечения уже давно хвалят Test Driven Development (TDD, разработка через тестирование). Однако в последнее время было сказано много резких слов в адрес TDD, поскольку его обвиняют в плохом проектировании программного обеспечения и невыполнении многих своих обещаний. Кульминацией этой тенденции стал пост Дэвида Хайнемайера Ханссона «TDD is dead. Long live testing.» (TDD мертв. Да здравствует тестирование).
Как это возможно, что одна и та же техника, которая так выгодна для стольких разработчиков, так губительна для других? В этой статье Владислав Кононов расскажет о трех заблуждениях, которые могли бы объяснить это явление.
Начнем с самого тонкого и самого деструктивного.
TDD это не «Проектирование через тестирование»
TDD расшифровывается как “Разработка через тестирование”. К сожалению, многие неверно истолковывают это как “Проектирование, основанное на тестировании”. Эта неточность может показаться невинной, но поверьте мне, это не так. Позвольте мне объяснить.
Проектирование, основанное на тестирование, подразумевает, что автоматические тесты должны определять ваши решения по разработке программного обеспечения. Серьёзно? При всем уважении, автоматические тесты не являются конечной целью разработки программного обеспечения. Истинная цель - выполнение проектов в срок, в соответствии с бюджетом и, самое главное, в соответствии со всеми требованиями к качеству. Именно на это должны быть направлены все ваши усилия по проектированию и разработке.
Если вы разрабатываете в первую очередь для тестируемости, вы получаете то, за что платите, — тестируемый код. Чаще всего этот дизайн будет полностью не связан с бизнес-областью и требованиями проекта. Он будет напоминать огромный граф объектов, полный случайных сложностей... но он будет проверяемым. Тестируемый тестами, которые тонут в моках (имеется в виду mock как тестовый двойник), и полностью сломается после изменения одного кусочка в реализации. Это то, что называется “повреждением, вызванным тестом”, и это ярко показано в блоге Дэвида Хайнемайера Ханссона «TDD is dead. Long live testing.»:
Нынешний фанатичный опыт TDD приводит к тому, что основное внимание уделяется модульным тестам, потому что это тесты, способные управлять дизайном кода (первоначальное обоснование для test-first – сначала тестирование, потом реализация). Я не думаю, что это здорово. Test-first приводят к чрезмерно сложной сети промежуточных объектов и косвенных обращений, чтобы избежать «медленных» действий. Например, попасть в базу данных. Или файл IO. Или пройти через браузер, чтобы протестировать всю систему. Это породило некоторые поистине ужасные архитектурные уродства. Густые джунгли служебных объектов, командных шаблонов и прочего.
Как должно быть? Ваш бизнес-домен должен определять ваши решения по проектированию. Выберите реализацию, которая наилучшим образом соответствует проблеме, которую вы пытаетесь решить. Нет смысла в полноценной модели домена, если все, что вам нужно, - это обычный интерфейс CRUD - вместо этого реализуйте шаблон Active Record. Если все, что вам нужно, это сценарий ETL, используйте шаблон Transaction Script.
Как вообще может иметь смысл решать все проблемы одним и тем же решением - гексагональной архитектурой и моделью предметной области? «Потому что этот дизайн идеально подходит для модульных тестов!» Понятно. Пора поговорить о втором заблуждении.
TDD это не (только) о модульных тестах
Широко распространено мнение, что, если вы используете TDD, вам следует писать модульные тесты. В этом нет никакого смысла. Модульные тесты - это не волшебная пуля, и, кстати, если вы посмотрите на определение TDD в Википедии, вы ничего не найдете о модульных тестах:
Разработка через тестирование (TDD) - это процесс разработки программного обеспечения, который основан на повторении очень короткого цикла разработки: сначала разработчик пишет (изначально неудачный) автоматизированный тестовый сценарий, который определяет желаемое улучшение или новую функцию, затем создает минимальный объем кода для прохождения этого теста и, наконец, преобразует новый код в приемлемые стандарты.
Основное внимание уделяется автоматическим тестам, и их можно разделить на три типа: модульные тесты, интеграционные тесты и сквозные тесты. Я не верю, что каждый проект нуждается в каждом из них. Опять же, это решение должно определяться вашей проблемной областью:
Вы имеете дело со сложной бизнес-логикой? Вам действительно нужны модульные тесты здесь.
Вы выполняете только простые операции CRUD? Используйте интеграционные тесты или сквозные тесты.
Сценарий ETL? Достаточно сквозных тестов.
Выберите стратегию тестирования, которая наилучшим образом соответствует вашему домену. Сначала напишите свои тесты, и вуаля - вы выполняете TDD и не позволяете тестам сбивать ваше проектирование с пути.
...И, говоря о модульных тестах, что вообще такое модуль? Переходим к третьему заблуждению.
Unit != Class
Еще одно распространенное заблуждение заключается в том, что модульные тесты должны проверять отдельные классы, и все зависимости класса должны быть имитированы. Такой подход является неточным. Это рецепт для сильной связи между тестами и реализацией. Эта связь подорвет все ваши усилия по рефакторингу, нарушив тем самым одно из фундаментальных обещаний TDD.
Определение модуля, которое мне нравится больше всего, принадлежит Рою Ошерову, автору книги The Art of Unit Testing:
Модульный тест - это автоматизированный фрагмент кода, который вызывает единицу работы в системе, а затем проверяет одно предположение о поведении этой единицы работы.
Единица работы (a unit of work) - это единый логический функциональный вариант использования (use case) в системе, который может быть вызван некоторым общедоступным интерфейсом (в большинстве случаев). Единица работы может охватывать один метод, целый класс или несколько классов, работающих вместе для достижения одной логической цели, которую можно проверить.
Функциональные варианты тестирования отделяют тесты от реализации. Это сделает рефакторинг возможным и потребует значительно меньше тестовых двойников.
Отсутствие буквы D в TDD
В конечном счете, есть еще одно наблюдение, которым я хочу поделиться, потому что оно суммирует все вышеупомянутые заблуждения.
Общепризнанно, что хорошо спроектированный код также поддается тестированию. Однако это соотношение не является коммутативным: хорошо спроектированный код можно тестировать, но не весь тестируемый код хорошо спроектирован. Доказательство тривиально:
Как вы можете идентифицировать проверяемый код? Легко - по тому, есть тесты или нет.
Как вы можете оценить качество проектирования? Извините, здесь нет ярлыков - все зависит от контекста. Хорошо продуманное решение для одного проекта - это чрезмерное усложнение для другого. А чрезмерное усложнение для одной области - это халатность для более сложной.
Поэтому, даже если реализация поддается тестированию, она все равно может не соответствовать решаемой проблеме и бизнес-области. Следовательно, отсутствующая буква “D” в TDD является “Доменом” бизнеса/проблемы. Вот почему я считаю, что DDD является необходимым условием для разработки на основе тестирования. Методология DDD применима не только к сложным моделям предметной области - напротив, она определяет набор рекомендаций по выбору наилучшего инструмента для работы в соответствии с проблемной областью. Но это тема для совершенно другой статьи.
P.S TDD 2.0
TDD был «заново открыт» Кентом Беком более десяти лет назад. Возможно, пора снова открыть TDD. Помимо модульных тестов, новая спецификация должна касаться других типов автоматизированных тестов, которые в то время были недоступны. И, конечно же, вместо того, чтобы работать против, TDD должен тесно сотрудничать с бизнес-областью.