Разработка через тестирование ( TDD ) — это способ написания кода , который включает в себя написание автоматизированного тестового случая на уровне модуля , который не проходит, затем написание кода, достаточного для прохождения теста, затем рефакторинг как тестового кода, так и производственного кода, а затем повторение с другим новым тестовым случаем.
Альтернативные подходы к написанию автоматизированных тестов — это написание всего производственного кода перед началом тестового кода или написание всего тестового кода перед началом производственного кода. При TDD оба пишутся вместе, что сокращает время отладки. [1]
TDD связана с концепциями программирования «сначала тест» экстремального программирования , начатыми в 1999 году [2] , но в последнее время вызвавшими более широкий интерес как таковые. [3]
Программисты также применяют эту концепцию для улучшения и отладки устаревшего кода, разработанного с использованием старых технологий. [4]
Инженер-программист Кент Бек , которому приписывают разработку или «переоткрытие» [5] этой техники, заявил в 2003 году, что TDD поощряет простые проекты и внушает доверие. [6]
Первоначальное описание TDD было в древней книге о программировании. В ней говорилось, что вы берете входную ленту, вручную вводите на выходную ленту ожидаемый вами вывод, затем программируете до тех пор, пока фактическая выходная лента не будет соответствовать ожидаемому выводу. После того, как я написал первый фреймворк xUnit на Smalltalk, я вспомнил, что читал это, и попробовал. Для меня это было началом TDD. Когда я описывал TDD старшим программистам, я часто слышу: «Конечно. А как еще вы могли бы программировать?» Поэтому я называю свою роль «заново открывающим» TDD.
— Кент Бек , «Почему Кент Бек говорит о «повторном открытии» разработки через тестирование? Какова история разработки через тестирование до повторного открытия Кентом Беком?» [7]
Шаги TDD несколько различаются по количеству и описанию у разных авторов, но в целом они следующие. Они основаны на книге Test-Driven Development by Example [ 6] и статье Кента Бека Canon TDD [8] .
Каждый тест должен быть небольшим, а коммиты должны делаться часто. Если новый код не проходит некоторые тесты, программист может отменить или вернуться, а не отлаживать чрезмерно.
При использовании внешних библиотек важно не писать тесты, которые настолько малы, что эффективно проверяют только саму библиотеку [3], если только нет оснований полагать, что библиотека содержит ошибки или недостаточно функциональна, чтобы удовлетворить все потребности разрабатываемого программного обеспечения.
TDD был принят за пределами разработки программного обеспечения, как в командах по продуктам, так и в командах по обслуживанию, как работа, основанная на тестировании . [9] Для успешного тестирования его необходимо проводить на микро- и макроуровнях. Каждый метод в классе, каждое значение входных данных, сообщение журнала и код ошибки, среди других точек данных, должны быть протестированы. [10] Подобно TDD, команды, не занимающиеся разработкой программного обеспечения, разрабатывают проверки контроля качества (QC) (обычно ручные тесты, а не автоматизированные тесты) для каждого аспекта работы до начала. Затем эти проверки QC используются для информирования о проекте и проверки связанных с ним результатов. Шесть шагов последовательности TDD применяются с небольшими семантическими изменениями:
Существуют различные аспекты использования разработки через тестирование, например, принципы «упрости, глупый» ( KISS ) и « тебе это не понадобится » (YAGNI). Сосредоточившись на написании только необходимого для прохождения тестов кода, проекты часто могут быть чище и понятнее, чем это достигается другими методами. [6] В книге «Разработка через тестирование на примере » Кент Бек также предлагает принцип « Притворяйся, пока не сделаешь ».
Чтобы достичь некоторой продвинутой концепции дизайна, такой как шаблон дизайна , пишутся тесты, которые генерируют этот дизайн. Код может оставаться проще целевого шаблона, но все равно проходить все требуемые тесты. Это может тревожить поначалу, но позволяет разработчику сосредоточиться только на том, что важно.
Сначала напишите тесты: тесты должны быть написаны до функциональности, которая должна быть протестирована. Утверждается, что это имеет много преимуществ. Это помогает гарантировать, что приложение написано для возможности тестирования, так как разработчики должны продумать, как протестировать приложение с самого начала, а не добавлять его позже. Это также гарантирует, что тесты для каждой функции будут написаны. Кроме того, написание тестов в первую очередь приводит к более глубокому и раннему пониманию требований к продукту, обеспечивает эффективность тестового кода и поддерживает постоянную фокусировку на качестве программного обеспечения . [11] При написании кода, ориентированного на функции, разработчики и организации склонны подталкивать разработчика к следующей функции, даже полностью игнорируя тестирование. Первый тест TDD может даже не скомпилироваться сначала, потому что требуемые ему классы и методы могут еще не существовать. Тем не менее, этот первый тест функционирует как начало исполняемой спецификации. [12]
Каждый тестовый случай изначально терпит неудачу: это гарантирует, что тест действительно работает и может обнаружить ошибку. Как только это будет показано, может быть реализована базовая функциональность. Это привело к «мантре разработки через тестирование», которая звучит как «красный/зеленый/рефакторинг», где красный означает провал , а зеленый означает прохождение . Разработка через тестирование постоянно повторяет шаги добавления тестовых случаев, которые терпят неудачу, их прохождения и рефакторинга. Получение ожидаемых результатов теста на каждом этапе укрепляет ментальную модель кода разработчика, повышает уверенность и увеличивает производительность.
Тестовому коду необходим доступ к тестируемому коду, но тестирование не должно нарушать обычные цели проектирования, такие как сокрытие информации , инкапсуляция и разделение интересов . Поэтому код модульного теста обычно располагается в том же проекте или модуле , что и тестируемый код.
В объектно-ориентированном проектировании это все еще не обеспечивает доступ к закрытым данным и методам. Поэтому для модульных тестов может потребоваться дополнительная работа. В Java и других языках разработчик может использовать рефлексию для доступа к закрытым полям и методам. [13] В качестве альтернативы внутренний класс может использоваться для хранения модульных тестов, чтобы они имели видимость членов и атрибутов включающего класса. В .NET Framework и некоторых других языках программирования частичные классы могут использоваться для предоставления закрытых методов и данных для доступа тестов.
Важно, чтобы такие тестовые хаки не оставались в производственном коде. В C и других языках директивы компилятора, такие как , #if DEBUG ... #endif
могут быть размещены вокруг таких дополнительных классов и, конечно, всего другого кода, связанного с тестированием, чтобы предотвратить их компиляцию в выпущенный код. Это означает, что выпущенный код не совсем такой же, как тот, который был протестирован модулем. Регулярное выполнение меньшего количества, но более полных, сквозных интеграционных тестов в окончательной сборке выпуска может гарантировать (помимо прочего), что не существует производственного кода, который тонко зависит от аспектов тестового оборудования.
Среди практиков TDD ведутся споры, задокументированные в их блогах и других работах, о том, разумно ли вообще тестировать частные методы и данные. Некоторые утверждают, что частные члены — это просто деталь реализации, которая может измениться, и им должно быть разрешено делать это без нарушения ряда тестов. Таким образом, должно быть достаточно протестировать любой класс через его открытый интерфейс или через интерфейс его подкласса, который некоторые языки называют «защищенным» интерфейсом. [14] Другие говорят, что критически важные аспекты функциональности могут быть реализованы в частных методах, и их прямое тестирование дает преимущество меньших и более прямых модульных тестов. [15] [16]
Модульные тесты так называются, потому что каждый из них проверяет одну единицу кода. Сложный модуль может иметь тысячу модульных тестов, а простой модуль может иметь только десять. Модульные тесты, используемые для TDD, никогда не должны пересекать границы процесса в программе, не говоря уже о сетевых соединениях. Это приводит к задержкам, из-за которых тесты выполняются медленно и отпугивают разработчиков от запуска всего пакета. Введение зависимостей от внешних модулей или данных также превращает модульные тесты в интеграционные тесты . Если один модуль ведет себя неправильно в цепочке взаимосвязанных модулей, не так сразу понятно, где искать причину сбоя.
Когда разрабатываемый код опирается на базу данных, веб-сервис или любой другой внешний процесс или сервис, обеспечение разделения с возможностью модульного тестирования также является возможностью и движущей силой для разработки более модульного, более тестируемого и более повторно используемого кода. [17] Необходимо два шага:
Методы поддельных и фиктивных объектов, которые возвращают данные, якобы из хранилища данных или пользователя, могут помочь в процессе тестирования, всегда возвращая одни и те же реалистичные данные, на которые могут полагаться тесты. Их также можно установить в предопределенные режимы сбоя, чтобы можно было разработать и надежно протестировать процедуры обработки ошибок. В режиме сбоя метод может возвращать недействительный, неполный или нулевой ответ или может выдавать исключение . Поддельные службы, отличные от хранилищ данных, также могут быть полезны в TDD: поддельная служба шифрования может на самом деле не шифровать передаваемые данные; поддельная служба случайных чисел может всегда возвращать 1. Поддельные или фиктивные реализации являются примерами внедрения зависимостей .
Тестовый двойник — это тестовая специфичная возможность, которая заменяет системную возможность, обычно класс или функцию, от которой зависит UUT. Тестовые двойники могут быть введены в систему в двух случаях: при компоновке и выполнении. Замена во время компоновки происходит, когда тестовый двойник компилируется в загрузочный модуль, который выполняется для проверки тестирования. Этот подход обычно используется при запуске в среде, отличной от целевой среды, которая требует двойников для кода аппаратного уровня для компиляции. Альтернативой замене компоновщика является замена во время выполнения, при которой реальная функциональность заменяется во время выполнения тестового случая. Эта замена обычно выполняется путем переназначения известных указателей на функции или замены объекта.
Тестовые двойники бывают разных типов и разной сложности:
Следствием такого внедрения зависимостей является то, что фактическая база данных или другой код внешнего доступа никогда не тестируется самим процессом TDD. Чтобы избежать ошибок, которые могут возникнуть из-за этого, необходимы другие тесты, которые создают экземпляр тестового кода с «реальными» реализациями интерфейсов, обсуждаемых выше. Это интеграционные тесты , и они довольно сильно отличаются от модульных тестов TDD. Их меньше, и они должны запускаться реже, чем модульные тесты. Тем не менее, их можно реализовать с использованием той же самой среды тестирования.
Интеграционные тесты, которые изменяют любое постоянное хранилище или базу данных, всегда должны быть тщательно разработаны с учетом начального и конечного состояния файлов или базы данных, даже если какой-либо тест не пройден. Это часто достигается с помощью некоторой комбинации следующих методов:
TearDown
, который является неотъемлемой частью многих тестовых фреймворков.try...catch...finally
структуры обработки исключений , где это возможно.Для TDD блок чаще всего определяется как класс или группа связанных функций, часто называемых модулем. Утверждается, что сохранение относительно небольших блоков обеспечивает критические преимущества, в том числе:
Расширенные практики разработки через тестирование могут привести к разработке через приемочные тесты (ATDD) и спецификации на примере , где критерии, указанные заказчиком, автоматизированы в приемочные тесты, которые затем управляют традиционным процессом разработки через модульное тестирование (UTDD). [18] Этот процесс гарантирует заказчику автоматизированный механизм для принятия решения о том, соответствует ли программное обеспечение его требованиям. С ATDD у команды разработчиков теперь есть конкретная цель для удовлетворения — приемочные тесты — которые постоянно держат их в фокусе на том, что заказчик действительно хочет от каждой пользовательской истории.
Эффективная компоновка тестового случая гарантирует выполнение всех требуемых действий, улучшает читаемость тестового случая и сглаживает поток выполнения. Последовательная структура помогает в построении самодокументирующего тестового случая. Обычно применяемая структура для тестовых случаев имеет (1) настройку, (2) выполнение, (3) проверку и (4) очистку.
Некоторые лучшие практики, которым может следовать отдельный человек, заключаются в том, чтобы разделить общую логику настройки и демонтажа на службы поддержки тестирования, используемые соответствующими тестовыми случаями, чтобы каждый тестовый оракул был сосредоточен только на результатах, необходимых для проверки его теста, и разрабатывать тесты, связанные со временем, чтобы обеспечить допуск для выполнения в операционных системах не в реальном времени. Распространенная практика предоставления 5-10-процентного запаса для позднего выполнения снижает потенциальное количество ложных отрицательных результатов при выполнении теста. Также предлагается относиться к тестовому коду с таким же уважением, как и к производственному коду. Тестовый код должен работать правильно как для положительных, так и для отрицательных случаев, служить долго, быть читаемым и поддерживаемым. Команды могут собираться вместе и просматривать тесты и методы тестирования, чтобы делиться эффективными методами и выявлять плохие привычки. [19]
Разработка через тестирование связана с разработкой через приемочное тестирование (ATDD), но отличается от нее. [20] TDD — это в первую очередь инструмент разработчика, помогающий создавать хорошо написанную единицу кода (функцию, класс или модуль), которая правильно выполняет набор операций. ATDD — это инструмент общения между заказчиком, разработчиком и тестировщиком для обеспечения четкого определения требований. TDD требует автоматизации тестирования. ATDD этого не делает, хотя автоматизация помогает с регрессионным тестированием. Тесты, используемые в TDD, часто могут быть получены из тестов ATDD, поскольку единицы кода реализуют некоторую часть требования. Тесты ATDD должны быть читаемыми заказчиком. Тесты TDD не должны быть такими.
BDD ( разработка на основе поведения ) объединяет практики из TDD и из ATDD. [21] Он включает практику написания тестов в первую очередь, но фокусируется на тестах, которые описывают поведение, а не на тестах, которые проверяют единицу реализации. Такие инструменты, как JBehave, Cucumber , Mspec и Specflow, предоставляют синтаксисы, которые позволяют владельцам продукта, разработчикам и инженерам по тестированию совместно определять поведение, которое затем может быть переведено в автоматизированные тесты.
Существует множество фреймворков и инструментов тестирования, полезных в TDD.
Разработчики могут использовать автоматизированные фреймворки тестирования , обычно называемые xUnit (которые являются производными от SUnit, созданного в 1998 году), для создания и автоматического запуска тестовых случаев. Фреймворки xUnit предоставляют возможности проверки тестов в стиле утверждений и отчеты о результатах. Эти возможности имеют решающее значение для автоматизации, поскольку они переносят бремя проверки выполнения с независимой постобработки на ту, которая включена в выполнение теста. Фреймворк выполнения, предоставляемый этими тестовыми фреймворками, позволяет автоматически выполнять все системные тестовые случаи или различные подмножества вместе с другими функциями. [22]
Тестовые фреймворки могут принимать выходные данные модульного теста в не зависящем от языка протоколе Test Anything, созданном в 1987 году.
Применение TDD на больших, сложных системах требует модульной архитектуры, четко определенных компонентов с опубликованными интерфейсами и дисциплинированной системной иерархии с максимизацией независимости платформы. Эти проверенные практики обеспечивают повышенную тестируемость и облегчают применение автоматизации сборки и тестирования. [11]
Сложные системы требуют архитектуры, которая соответствует ряду требований. Ключевой подмножество этих требований включает поддержку полного и эффективного тестирования системы. Эффективная модульная конструкция дает компоненты, которые разделяют черты, необходимые для эффективного TDD.
Ключевым методом построения эффективной модульной архитектуры является моделирование сценариев, при котором создается набор диаграмм последовательности, каждая из которых фокусируется на одном сценарии выполнения на уровне системы. Модель сценария предоставляет превосходное средство для создания стратегии взаимодействия между компонентами в ответ на определенный стимул. Каждая из этих моделей сценария служит богатым набором требований к услугам или функциям, которые должен предоставлять компонент, а также диктует порядок, в котором эти компоненты и услуги взаимодействуют друг с другом. Моделирование сценариев может значительно облегчить построение тестов TDD для сложной системы. [11]
В более крупной системе влияние низкого качества компонентов усиливается сложностью взаимодействий. Это усиление заставляет преимущества TDD накапливаться еще быстрее в контексте более крупных проектов. Однако сложность общей популяции тестов может стать проблемой сама по себе, подрывая потенциальные выгоды. Это звучит просто, но ключевым начальным шагом является признание того, что тестовый код также является важным программным обеспечением и должен создаваться и поддерживаться с той же строгостью, что и производственный код.
Создание и управление архитектурой тестового ПО в рамках сложной системы так же важно, как и архитектура основного продукта. Тестовые драйверы взаимодействуют с UUT, тестовыми двойниками и фреймворком модульного тестирования. [11]
Разработка через тестирование (TDD) — это подход к разработке программного обеспечения, при котором тесты пишутся до написания фактического кода. Он предлагает несколько преимуществ:
Однако TDD не лишена недостатков:
Исследование 2005 года показало, что использование TDD означало написание большего количества тестов, и, в свою очередь, программисты, которые писали больше тестов, как правило, были более продуктивны. [25] Гипотезы, касающиеся качества кода и более прямой корреляции между TDD и производительностью, оказались неубедительными. [26]
Программисты, использующие чистый TDD на новых (« greenfield ») проектах, сообщали, что они лишь изредка испытывали необходимость в вызове отладчика . При использовании в сочетании с системой контроля версий , когда тесты неожиданно терпят неудачу, возврат кода к последней версии, которая прошла все тесты, часто может быть более продуктивным, чем отладка. [27]
Разработка через тестирование предлагает больше, чем просто проверку правильности, но также может управлять дизайном программы. [28] Сосредоточившись сначала на тестовых случаях, нужно представить, как функциональность используется клиентами (в первом случае, тестовые случаи). Таким образом, программист озабочен интерфейсом до реализации. Это преимущество является дополнительным к проектированию по контракту , поскольку оно подходит к коду через тестовые случаи, а не через математические утверждения или предубеждения.
Разработка через тестирование дает возможность делать небольшие шаги, когда это необходимо. Она позволяет программисту сосредоточиться на текущей задаче, поскольку первой целью является прохождение теста. Исключительные случаи и обработка ошибок изначально не рассматриваются, а тесты для создания этих внешних обстоятельств реализуются отдельно. Разработка через тестирование гарантирует, что весь написанный код покрыт по крайней мере одним тестом. Это дает команде программистов и последующим пользователям большую уверенность в коде.
Хотя верно, что при TDD требуется больше кода, чем без TDD из-за кода модульного теста, общее время реализации кода может быть короче на основе модели Мюллера и Падберга. [29] Большое количество тестов помогает ограничить количество дефектов в коде. Ранний и частый характер тестирования помогает обнаружить дефекты на ранних этапах цикла разработки, не давая им стать эндемичными и дорогостоящими проблемами. Устранение дефектов на ранних этапах процесса обычно позволяет избежать длительной и утомительной отладки на более поздних этапах проекта.
TDD может привести к более модульному, гибкому и расширяемому коду. Этот эффект часто возникает, потому что методология требует, чтобы разработчики думали о программном обеспечении в терминах небольших модулей, которые могут быть написаны и протестированы независимо и позже интегрированы вместе. Это приводит к меньшим, более сфокусированным классам, более слабой связанности и более чистым интерфейсам. Использование шаблона проектирования фиктивных объектов также способствует общей модуляризации кода, потому что этот шаблон требует, чтобы код был написан таким образом, чтобы модули можно было легко переключать между фиктивными версиями для модульного тестирования и «реальными» версиями для развертывания.
Поскольку не пишется больше кода, чем необходимо для прохождения неудавшегося тестового случая, автоматизированные тесты, как правило, охватывают каждый путь кода. Например, чтобы разработчик TDD добавил ветвь else
к существующему if
оператору, разработчику сначала придется написать неудавшийся тестовый случай, который мотивирует ветвь. В результате автоматизированные тесты, полученные в результате TDD, как правило, очень тщательны: они обнаруживают любые неожиданные изменения в поведении кода. Это обнаруживает проблемы, которые могут возникнуть, когда изменение на более позднем этапе цикла разработки неожиданно изменяет другую функциональность.
Мадейски [30] предоставил эмпирические доказательства (через серию лабораторных экспериментов с более чем 200 разработчиками) относительно превосходства практики TDD над традиционным подходом Test-Last или подходом тестирования на корректность в отношении более низкой связи между объектами (CBO). Средний размер эффекта представляет собой средний (но близкий к большому) эффект на основе метаанализа проведенных экспериментов, что является существенным выводом. Он предполагает лучшую модуляризацию (т. е. более модульную конструкцию), более легкое повторное использование и тестирование разработанных программных продуктов благодаря практике программирования TDD. [30] Мадейски также измерил эффект практики TDD на модульные тесты с использованием покрытия ветвей (BC) и индикатора оценки мутации (MSI), [31] [32] [33], которые являются индикаторами тщательности и эффективности обнаружения неисправностей модульных тестов соответственно. Размер эффекта TDD на покрытие ветвей был средним по размеру и, следовательно, считается существенным эффектом. [30] Эти выводы были впоследствии подтверждены дальнейшими, меньшими экспериментальными оценками TDD. [34] [35] [36] [37]
Разработка через тестирование не выполняет достаточного тестирования в ситуациях, когда для определения успеха или неудачи требуются полные функциональные тесты, из-за широкого использования модульных тестов. [38] Примерами этого являются пользовательские интерфейсы , программы, работающие с базами данных , и некоторые, зависящие от определенных сетевых конфигураций. TDD призывает разработчиков помещать минимальный объем кода в такие модули и максимизировать логику, которая находится в тестируемом библиотечном коде, используя подделки и макеты для представления внешнего мира. [39]
Поддержка руководства имеет важное значение. Если вся организация не верит, что разработка через тестирование улучшит продукт, руководство может посчитать, что время, потраченное на написание тестов, тратится впустую. [40]
Модульные тесты, созданные в среде разработки через тестирование, обычно создаются разработчиком, который пишет тестируемый код. Поэтому тесты могут разделять слепые пятна с кодом: если, например, разработчик не понимает, что определенные входные параметры должны быть проверены, скорее всего, ни тест, ни код не будут проверять эти параметры. Другой пример: если разработчик неверно истолкует требования к разрабатываемому им модулю, то код и написанные им модульные тесты будут одинаково неверны. Поэтому тесты пройдут, что даст ложное ощущение правильности.
Большое количество пройденных модульных тестов может создать ложное чувство безопасности, что приведет к сокращению дополнительных мероприятий по тестированию программного обеспечения , таких как интеграционное тестирование и тестирование на соответствие .
Тесты становятся частью накладных расходов на обслуживание проекта. Плохо написанные тесты, например, те, которые включают жестко закодированные строки ошибок, сами по себе склонны к сбоям, и их поддержка обходится дорого. Это особенно касается хрупких тестов. [41] Существует риск того, что тесты, которые регулярно генерируют ложные сбои, будут проигнорированы, так что когда произойдет реальный сбой, он может быть не обнаружен. Можно написать тесты для простого и легкого обслуживания, например, путем повторного использования строк ошибок, и это должно быть целью на этапе рефакторинга кода , описанном выше.
Написание и поддержка избыточного количества тестов требует времени. Кроме того, более гибкие модули (с ограниченными тестами) могут принимать новые требования без необходимости изменения тестов. По этим причинам тестирование только для экстремальных условий или небольшой выборки данных может быть проще в настройке, чем набор очень подробных тестов.
Уровень покрытия и детализации тестирования, достигнутый в ходе повторных циклов TDD, не может быть легко воссоздан позднее. Поэтому эти оригинальные или ранние тесты становятся все более ценными с течением времени. Тактика заключается в том, чтобы исправить это на ранней стадии. Кроме того, если плохая архитектура, плохой дизайн или плохая стратегия тестирования приводят к позднему изменению, из-за которого десятки существующих тестов терпят неудачу, важно, чтобы они были индивидуально исправлены. Простое удаление, отключение или необдуманное изменение может привести к необнаруживаемым дырам в тестовом покрытии.
Первая конференция TDD прошла в июле 2021 года. [42] Конференции были записаны на YouTube [43]
Мы обнаружили, что студенты, которые в среднем писали больше тестов, и, в свою очередь, студенты, которые писали больше тестов, как правило, были более продуктивны.
Так что связь TDD с качеством в лучшем случае проблематична. Ее связь с производительностью более интересна. Я надеюсь, что будет последующее исследование, потому что цифры производительности просто не очень хорошо складываются для меня. Существует неоспоримая корреляция между производительностью и количеством тестов, но эта корреляция на самом деле сильнее в группе без TDD (где был один
выброс
по сравнению с примерно половиной группы TDD, находящейся за пределами диапазона 95%).
Сравнивая [TDD] с подходом к разработке без тестирования, вы заменяете все мысленные проверки и пошаговое выполнение отладчика кодом, который проверяет, что ваша программа делает именно то, что вы от нее хотели.