Введение

Метод кодирования, основанный на повторении циклов написания тестового кода, а затем производственного кода. Разработка через тестирование (TDD) – это подход к написанию кода, который заключается в написании автоматизированного модульного теста, который изначально не проходит, затем написании минимально необходимого кода для его успешного выполнения, после чего рефакторинге как тестового, так и производственного кода, и повторении этого цикла с новым тестом. Альтернативные подходы к написанию автоматизированных тестов – это написание всего производственного кода перед началом работы над тестами, или написание всех тестов перед началом работы над производственным кодом. В TDD написание тестов и производственного кода происходит одновременно. TDD восходит к концепциям "тестирование прежде всего" в экстремальном программировании, начатом в 1999 году, но в последнее время приобрел самостоятельный интерес. Программисты также используют этот подход для улучшения и отладки устаревшего кода, разработанного с использованием более ранних методов.

Стиль разработки

Существуют различные аспекты использования разработки, основанной на тестировании, например, принципы "сохраняйте простоту, глупость" (KISS) и "Вы не будете в этом нуждаться" (YAGNI). Сосредоточившись на написании только кода, необходимого для успешного прохождения тестов, можно добиться более чистого и понятного дизайна, чем при использовании других подходов. При разработке, начинающейся с реализации функциональности, у разработчиков и организаций часто возникает тенденция переходить к следующей задаче, даже полностью игнорируя тестирование. Первый тест в TDD может даже не компилироваться, поскольку необходимые классы и методы могут еще не существовать. Тем не менее, этот первый тест служит началом исполняемой спецификации. Каждый тест изначально проваливается: это гарантирует, что тест действительно работает и способен обнаруживать ошибки. После подтверждения этого, можно приступать к реализации основной функциональности. Это привело к появлению "мантры разработки, основанной на тестировании" – "красный/зеленый/рефакторинг", где "красный" означает провал, а "зеленый" – успех. Разработка, основанная на тестировании, постоянно повторяет циклы: добавление тестов, которые изначально проваливаются, их успешное выполнение и рефакторинг. Получение ожидаемых результатов тестирования на каждом этапе укрепляет понимание кода разработчиком, повышает уверенность и увеличивает производительность.

Видимость кода

Испытательный код нуждается в доступе к коду, который он тестирует, но тестирование не должно нарушать основные принципы проектирования, такие как сокрытие информации, инкапсуляция и разделение ответственности. Поэтому код модульных тестов обычно располагается в том же проекте или модуле, что и тестируемый код. В объектно-ориентированном проектировании это всё равно не обеспечивает доступа к приватным данным и методам. Следовательно, для модульных тестов может потребоваться дополнительная работа. В Java и других языках разработчик может использовать рефлексию для доступа к приватным полям и методам. Альтернативно, внутренний класс можно использовать для размещения модульных тестов, чтобы они имели доступ к членам и атрибутам окружающего класса. В .NET Framework и некоторых других языках программирования частичные классы могут использоваться для предоставления доступа к приватным методам и данным для тестов. Важно, чтобы подобные "хаки" для тестирования не оставались в производственном коде. В C и других языках директивы компилятора, такие как #if DEBUG #endif, можно размещать вокруг таких дополнительных классов и, фактически, вокруг всего кода, связанного с тестированием, чтобы предотвратить его компиляцию в релизную версию. Это означает, что релизная версия кода не идентична коду, который подвергался модульному тестированию. Регулярное выполнение меньшего количества, но более полных сквозных интеграционных тестов на финальной релизной сборке может гарантировать (среди прочего), что в производственном коде нет скрытой зависимости от аспектов тестовой инфраструктуры. Среди практикующих TDD существуют споры, задокументированные в их блогах и других публикациях, относительно целесообразности тестирования приватных методов и данных в принципе. Некоторые утверждают, что приватные члены – это лишь детали реализации, которые могут измениться, и им следует позволить меняться без поломки множества тестов. Таким образом, должно быть достаточно тестировать любой класс через его публичный интерфейс или через интерфейс подкласса, который в некоторых языках называют "защищённым" интерфейсом. Другие считают, что критически важные аспекты функциональности могут быть реализованы в приватных методах, и их прямое тестирование даёт преимущество в виде более компактных и прямых модульных тестов.

Структура испытания

Эффективная организация тестового случая обеспечивает выполнение всех необходимых действий, повышает читаемость тестового случая и упрощает процесс выполнения. Последовательная структура способствует созданию самодокументирующегося тестового случая. Обычно применяемая структура для тестовых случаев включает (1) подготовку, (2) выполнение, (3) проверку и (4) завершение. Подготовка: привести испытываемый объект (UUT) или общую тестовую систему в состояние, необходимое для проведения теста. Выполнение: инициировать/управлять UUT для выполнения целевого поведения и зафиксировать все выходные данные, такие как возвращаемые значения и выходные параметры. Этот шаг обычно очень прост. Проверка: убедиться в корректности результатов теста. Эти результаты могут включать явные выходные данные, полученные во время выполнения, или изменения состояния в UUT. Завершение: восстановить UUT или общую тестовую систему в исходное состояние перед тестом. Это восстановление позволяет немедленно запустить следующий тест. В некоторых случаях, для сохранения информации для возможного анализа сбоев теста, завершение следует начинать непосредственно перед запуском подготовки к тесту. Тестирование деталей реализации. Медленные тесты.

TDD и ATDD

Разработка через тестирование связана с, но отличается от разработки через тестирование, основанной на приемочных тестах (ATDD). TDD – это прежде всего инструмент разработчика, помогающий создавать хорошо написанные единицы кода (функции, классы или модули), корректно выполняющие набор операций. ATDD – это инструмент коммуникации между заказчиком, разработчиком и тестировщиком для обеспечения четкого определения требований. TDD требует автоматизации тестирования. ATDD не требует этого, хотя автоматизация полезна для регрессионного тестирования. Тесты, используемые в TDD, часто могут быть выведены из тестов ATDD, поскольку единицы кода реализуют часть требования. Тесты ATDD должны быть понятны заказчику. Тесты TDD не обязательно должны быть понятны.

TDD и BDD

BDD (разработка через поведение) сочетает в себе практики TDD и ATDD. Она включает в себя практику написания тестов в первую очередь, но фокусируется на тестах, описывающих поведение, а не на тестах, проверяющих отдельный модуль реализации. Инструменты, такие как JBehave, Cucumber, Mspec и Specflow, предоставляют синтаксис, позволяющий владельцам продукта, разработчикам и инженерам по тестированию совместно определять поведение, которое затем может быть преобразовано в автоматизированные тесты.

Программное обеспечение для TDD

Существует множество фреймворков и инструментов для тестирования, которые полезны при разработке через тестирование (TDD).

Фреймворки xUnit

Разработчики могут использовать фреймворки автоматизированного тестирования, обычно объединяемые под общим названием xUnit (которые произошли от SUnit, разработанного в 1998 году), для создания и автоматического запуска тестов. Фреймворки xUnit предоставляют средства проверки результатов тестов на основе утверждений и формирования отчетов. Эти средства критически важны для автоматизации, поскольку перекладывают задачу проверки результатов выполнения из отдельного этапа постобработки внутрь процесса выполнения теста. Предоставляемый этими фреймворками механизм выполнения позволяет автоматически запускать все системные тесты или их различные подмножества, а также использовать другие функции.

Результаты TAP

Испытательные фреймворки могут принимать результаты модульных тестов в формате Test Anything Protocol, разработанном в 1987 году и не зависящем от языка программирования.

TDD для сложных систем

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

Преимущества

Исследование 2005 года показало, что использование TDD подразумевало написание большего количества тестов, и, как следствие, программисты, писавшие больше тестов, как правило, были более продуктивны. Гипотезы, касающиеся качества кода и более прямой корреляции между TDD и производительностью, оказались несостоятельными. Программисты, использующие чистый TDD в новых ("зеленых") проектах, отмечали, что редко испытывают необходимость в использовании отладчика. При использовании в сочетании с системой контроля версий, при неожиданном провале тестов, возврат кода к последней версии, успешно прошедшей все тесты, зачастую может быть более продуктивным, чем отладка. Разработка через тестирование (TDD) предлагает не только простую проверку корректности, но и может направлять проектирование программы. Сосредоточившись сначала на тестовых примерах, необходимо представить, как функциональность будет использоваться клиентами (в данном случае, тестовыми примерами). Таким образом, программист заботится об интерфейсе прежде, чем о реализации. Эта выгода дополняет разработку по контракту, поскольку она подходит к коду через тестовые примеры, а не через математические утверждения или предубеждения. Разработка через тестирование предоставляет возможность делать небольшие шаги при необходимости. Она позволяет программисту сосредоточиться на текущей задаче, поскольку первоочередная цель – заставить тест пройти. Исключительные случаи и обработка ошибок изначально не рассматриваются, а тесты для создания этих нештатных ситуаций реализуются отдельно. Таким образом, TDD гарантирует, что весь написанный код будет покрыт хотя бы одним тестом. Это дает команде программистов и последующим пользователям более высокий уровень уверенности в коде. Хотя верно, что TDD требует больше кода, чем разработка без TDD из-за кода модульных тестов, общее время реализации кода может быть меньше, согласно модели Мюллера и Падберга. Большое количество тестов помогает ограничить количество дефектов в коде. Ранний и частый характер тестирования помогает выявлять дефекты на ранних этапах цикла разработки, предотвращая их превращение в системные и дорогостоящие проблемы. Устранение дефектов на ранних этапах процесса обычно позволяет избежать длительной и утомительной отладки на более поздних этапах проекта. TDD может привести к более модульному, гибкому и расширяемому коду. Этот эффект часто возникает из-за того, что методология требует от разработчиков мыслить о программном обеспечении в терминах небольших, независимых модулей, которые можно написать и протестировать отдельно, а затем интегрировать вместе. Это приводит к созданию более компактных, целенаправленных классов, слабосвязанным компонентам и более чистым интерфейсам. Использование шаблона проектирования "заглушка" (mock object) также способствует общей модульности кода, поскольку этот шаблон требует, чтобы код был написан таким образом, чтобы модули можно было легко переключать между заглушками для модульного тестирования и "реальными" версиями для развертывания. Поскольку не пишется больше кода, чем необходимо для прохождения неудачного теста, автоматизированные тесты, как правило, охватывают каждый путь выполнения кода. Например, чтобы разработчик, использующий TDD, добавил ветвь `else` к существующему оператору `if`, он сначала должен написать неудачный тестовый пример, который обосновывает необходимость этой ветви. В результате автоматизированные тесты, полученные в результате TDD, как правило, очень тщательны: они обнаруживают любые неожиданные изменения в поведении кода. Это позволяет выявлять проблемы, которые могут возникнуть, когда изменение на более позднем этапе цикла разработки неожиданно влияет на другую функциональность. Мадейски предоставил эмпирические доказательства (в ходе серии лабораторных экспериментов с участием более 200 разработчиков) относительно превосходства практики TDD над традиционным подходом "Test Last" или тестированием на корректность, в отношении снижения связанности между объектами (CBO). Средний размер эффекта представляет собой средний (но близкий к большому) эффект на основе метаанализа проведенных экспериментов, что является значимым результатом. Это указывает на лучшую модульность (то есть более модульную структуру), более легкое повторное использование и тестирование разработанных программных продуктов благодаря практике программирования TDD, которые являются показателями тщательности и эффективности модульных тестов соответственно. Размер эффекта TDD на покрытие ветвей был средним и, следовательно, считается существенным эффектом.

Психологические преимущества для программиста

Повышенная уверенность: TDD позволяет программистам вносить изменения или добавлять новые функции с уверенностью. Знание того, что код постоянно тестируется, снижает страх сломать существующую функциональность. Эта страховка может стимулировать более инновационные и творческие подходы к решению задач. Снижение страха перед изменениями и стресса: в традиционной разработке изменение существующего кода может быть пугающим из-за риска внесения ошибок. TDD, с его полным набором тестов, уменьшает этот страх, поскольку тесты немедленно выявляют любые проблемы, вызванные изменениями. Осознание того, что кодовая база защищена тестами, может снизить стресс и тревогу, связанные с программированием. Разработчики могут чувствовать себя более спокойно и охотнее экспериментировать и проводить рефакторинг. Улучшенная концентрация: написание тестов в первую очередь помогает программистам сосредоточиться на требованиях и проектировании перед написанием кода. Эта концентрация может привести к более четкому и целенаправленному кодированию, поскольку разработчик всегда помнит о цели, которую он пытается достичь. Чувство достижения и удовлетворение от работы: успешное прохождение тестов дает быстрое и регулярное ощущение выполненного долга, повышая моральный дух. Это может быть особенно мотивирующим в долгосрочных проектах, где конечная цель может казаться далекой. Сочетание всех этих факторов может привести к повышению удовлетворенности работой. Когда разработчики чувствуют себя уверенно, сосредоточенно и являются частью сплоченной команды, их общая удовлетворенность работой может значительно возрасти.

Ограничения

Разработка через тестирование не обеспечивает достаточного тестирования в ситуациях, когда для определения успеха или неудачи требуются полноценные функциональные тесты, из-за широкого использования модульных тестов. Примерами таких ситуаций являются пользовательские интерфейсы, программы, работающие с базами данных, и некоторые, зависящие от специфических сетевых конфигураций. TDD стимулирует разработчиков помещать минимальное количество кода в такие модули и максимизировать логику в тестируемом коде библиотек, используя заглушки и моки для представления внешнего мира. Поддержка руководства необходима. Если вся организация не убеждена, что разработка через тестирование улучшит продукт, руководство может посчитать время, затраченное на написание тестов, потраченным впустую. Модульные тесты, создаваемые в среде разработки через тестирование, обычно пишутся разработчиком, который пишет тестируемый код. Следовательно, тесты могут иметь те же недостатки, что и код: если, например, разработчик не осознает, что определенные входные параметры необходимо проверять, скорее всего, ни тест, ни код не будут проверять эти параметры. Другой пример: если разработчик неверно интерпретирует требования к разрабатываемому модулю, код и модульные тесты, которые он пишет, будут ошибочными одинаково. Поэтому тесты будут проходить, создавая ложное ощущение корректности. Большое количество успешно пройденных модульных тестов может создать ложное чувство безопасности, что приведет к уменьшению количества дополнительных видов тестирования программного обеспечения, таких как интеграционное и приемочное тестирование. Тесты становятся частью затрат на сопровождение проекта. Плохо написанные тесты, например, содержащие жестко заданные строки ошибок, сами по себе подвержены сбоям и дороги в обслуживании. Это особенно актуально для хрупких тестов. Существует риск того, что тесты, регулярно выдающие ложные сбои, будут игнорироваться, и в случае реального сбоя он может остаться незамеченным. Возможно написание тестов, требующих минимального обслуживания, например, за счет повторного использования строк ошибок, и это должно быть целью на этапе рефакторинга кода, описанном выше. Написание и поддержка большого количества тестов требует времени. Кроме того, более гибкие модули (с ограниченным количеством тестов) могут принимать новые требования без необходимости изменения тестов. По этим причинам тестирование только крайних случаев или небольшой выборки данных может быть проще в адаптации, чем набор детализированных тестов. Уровень покрытия и детализация тестирования, достигнутые в ходе повторных циклов TDD, не могут быть легко воссозданы позднее. Поэтому эти первоначальные, или ранние, тесты со временем становятся все более ценными. Важно исправлять ошибки на ранних этапах. Кроме того, если плохая архитектура, неудачный дизайн или неэффективная стратегия тестирования приводят к позднему изменению, из-за которого не проходят десятки существующих тестов, важно исправить их по отдельности. Простое удаление, отключение или бездумное изменение тестов может привести к незаметным пробелам в покрытии тестами.

Конференция

Первая конференция TDD состоялась в июле 2021 года. Записи конференций доступны на YouTube.