Разработка через тестирование

Материал из Википедии — свободной энциклопедии
Перейти к: навигация, поиск
Разработка программного обеспечения
Процесс разработки ПО
Шаги процесса

Анализ • Проектирование • Программирование • Документирование • Тестирование

Модели

Итеративная • Спиральная • Каскадная • V-Model • Dual Vee Model

Методологии

Agile (XP, Lean, Scrum, FDD и др.) • Cleanroom • OpenUP • RAD • RUP • MSF • DSDM • TDD

Сопутствующие дисциплины

Конфигурационное управление • Управление проектами • Управление требованиями

Разработка через тестирование (англ. test-driven development, TDD) — техника разработки программного обеспечения, которая основывается на повторении очень коротких циклов разработки: сначала пишется тест, покрывающий желаемое изменение, затем пишется код, который позволит пройти тест, и под конец проводится рефакторинг нового кода к соответствующим стандартам. Кент Бек, считающийся изобретателем этой техники, утверждал в 2003 году, что разработка через тестирование поощряет простой дизайн и внушает уверенность (англ. inspires confidence)[1].

В 1999 году при своём появлении разработка через тестирование была тесно связана с концепцией «сначала тест» (англ. test-first), применяемой в экстремальном программировании[2], однако позже выделилась как независимая методология.[3].

Тест — это процедура, которая позволяет либо подтвердить, либо опровергнуть работоспособность кода. Когда программист проверяет работоспособность разработанного им кода, он выполняет тестирование вручную. В данном контексте тест состоит из двух этапов: стимулирование кода и проверка результатов его работы. Автоматический тест выполняется иначе: вместо программиста стимулированием кода и проверкой результатов занимается компьютер, который отображает на экране результат выполнения теста: код работоспособен или код неработоспособен. При этом происходит «инверсия ответственности»: от логики тестов и их качества зависит, будет ли код соответствовать техническому заданию. Методика разработки через тестирование заключается главным образом именно в организации автоматических тестов и выработке определённых навыков тестирования.

Требования[править | править исходный текст]

Разработка через тестирование требует от разработчика создания автоматизированных модульных тестов, определяющих требования к коду непосредственно перед написанием самого кода. Тест содержит проверки условий, которые могут либо выполняться, либо нет. Когда они выполняются, говорят, что тест пройден. Прохождение теста подтверждает поведение, предполагаемое программистом. Разработчики часто пользуются библиотеками для тестирования (англ. testing frameworks) для создания и автоматизации запуска наборов тестов. На практике модульные тесты покрывают критические и нетривиальные участки кода. Это может быть код, который подвержен частым изменениям, код, от работы которого зависит работоспособность большого количества другого кода, или код с большим количеством зависимостей.

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

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

Разумеется, к тестам применяются те же требования стандартов кодирования, что и к основному коду.

Цикл разработки через тестирование[править | править исходный текст]

Графическое представление цикла разработки, в виде блок-схемы

Приведенная последовательность действий основана на книге Кента Бека «Разработка через тестирование: на примере» (англ. Test Driven Development: By Example).[1]

Добавление теста[править | править исходный текст]

При разработке через тестирование, добавление каждой новой функциональности (англ. feature) в программу начинается с написания теста. Неизбежно этот тест не будет проходить, поскольку соответствующий код ещё не написан. (Если же написанный тест прошёл, это означает, что либо предложенная «новая» функциональность уже существует, либо тест имеет недостатки.) Чтобы написать тест, разработчик должен чётко понимать предъявляемые к новой возможности требования. Для этого рассматриваются возможные сценарии использования и пользовательские истории. Новые требования могут также повлечь изменение существующих тестов. Это отличает разработку через тестирование от техник, когда тесты пишутся после того, как код уже написан: она заставляет разработчика сфокусироваться на требованиях до написания кода — тонкое, но важное отличие.

Запуск всех тестов: убедиться, что новые тесты не проходят[править | править исходный текст]

На этом этапе проверяют, что только что написанные тесты не проходят. Этот этап также проверяет сами тесты: написанный тест может проходить всегда и соответственно быть бесполезным. Новые тесты должны не проходить по объяснимым причинам. Это увеличит уверенность (хотя не будет гарантировать полностью), что тест действительно тестирует то, для чего он был разработан.

Написать код[править | править исходный текст]

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

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

Запуск всех тестов: убедиться, что все тесты проходят[править | править исходный текст]

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

Рефакторинг[править | править исходный текст]

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

Повторить цикл[править | править исходный текст]

Описанный цикл повторяется, реализуя всё новую и новую функциональность. Шаги следует делать небольшими, от 1 до 10 изменений между запусками тестов. Если новый код не удовлетворяет новым тестам или старые тесты перестают проходить, программист должен вернуться к отладке. При использовании сторонних библиотек не следует делать настолько небольшие изменения, которые буквально тестируют саму стороннюю библиотеку[3], а не код, её использующий, если только нет подозрений, что библиотека содержит ошибки.

Стиль разработки[править | править исходный текст]

Разработка через тестирование тесно связана с такими принципами как «делай проще, дурачок» (англ. keep it simple, stupid, KISS) и «вам это не понадобится» (англ. you ain’t gonna need it, YAGNI). Дизайн может быть чище и яснее, при написании лишь того кода, который необходим для прохождения теста.[1] Кент Бек также предлагает принцип «подделай, пока не сделаешь» (англ. fake it till you make it). Тесты должны писаться для тестируемой функциональности. Считается, что это имеет два преимущества. Это помогает убедиться, что приложение пригодно для тестирования, поскольку разработчику придется с самого начала обдумать то, как приложение будет тестироваться. Это также способствует тому, что тестами будет покрыта вся функциональность. Когда функциональность пишется до тестов, разработчики и организации склонны переходить к реализации следующей функциональности, не протестировав существующую.

Идея проверять, что вновь написанный тест не проходит, помогает убедиться, что тест реально что-то проверяет. Только после этой проверки следует приступать к реализации новой функциональности. Этот приём, известный как «красный/зеленый/рефакторинг», называют «мантрой разработки через тестирование». Под красным здесь понимают не прошедшие тесты, а под зелёным — прошедшие.

Отработанные практики разработки через тестирование привели к созданию техники «разработка через приёмочное тестирование» (англ. Acceptance Test-driven development, ATDD), в котором критерии описанные заказчиком автоматизируются в приемочные тесты, используемые потом в обычном процессе разработки через модульное тестирование (англ. unit test-driven development, UTDD).[4] Этот процесс позволяет гарантировать, что приложение удовлетворяет сформулированным требованиям. При разработке через приёмочное тестирование, команда разработчиков сконцентрирована на четкой задаче: удовлетворить приемочные тесты, которые отражают соответствующие требования пользователя.

Приёмочные (функциональные) тесты (англ. customer tests, acceptance tests) — тесты, проверяющие функциональность приложения на соответствие требованиям заказчика. Приёмочные тесты проходят на стороне заказчика. Это помогает ему быть уверенным в том, что он получит всю необходимую функциональность.

Преимущества[править | править исходный текст]

Исследование 2005 года показало, что использование разработки через тестирование предполагает написание большего количества тестов, в свою очередь, программисты, пишущие больше тестов, склонны быть более продуктивными.[5] Гипотезы связывающие качество кода с TDD были неубедительны.[6]

Программисты, использующие TDD на новых проектах, отмечают, что они реже ощущают необходимость использовать отладчик. Если некоторые из тестов неожиданно перестают проходить, откат к последней версии, которая проходит все тесты, может быть более продуктивным, нежели отладка.[7]

Разработка через тестирование предлагает больше, чем просто проверку корректности, она также влияет на дизайн программы. Изначально сфокусировавшись на тестах, проще представить, какая функциональность необходима пользователю. Таким образом, разработчик продумывает детали интерфейса до реализации. Тесты заставляют делать свой код более приспособленным для тестирования. Например, отказываться от глобальных переменных, одиночек (singletons), делать классы менее связанными и легкими для использования. Сильно связанный код или код, который требует сложной инициализации, будет значительно труднее протестировать. Модульное тестирование способствует формированию четких и небольших интерфейсов. Каждый класс будет выполнять определенную роль, как правило небольшую. Как следствие, зависимости между классами будут снижаться, а зацепление повышаться. Контрактное программирование (англ. design by contract) дополняет тестирование, формируя необходимые требования через утверждения (англ. assertions).

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

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

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

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

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

Слабые места[править | править исходный текст]

  • Главным недостатком TDD является то, что к нему сложно привыкнуть, из-за ряда причин, включая увеличение за счёт него времени перед написанием основного кода программ и увеличение объёма кода в целом.[источник не указан 172 дня]
  • Существуют задачи, которые невозможно (по крайней мере, на текущий момент) решить только при помощи тестов. В частности, TDD не позволяет механически продемонстрировать адекватность разработанного кода в области безопасности данных и взаимодействия между процессами. Безусловно, безопасность основана на коде, в котором не должно быть дефектов, однако она основана также на участии человека в процедурах защиты данных. Тонкие проблемы, возникающие в области взаимодействия между процессами, невозможно с уверенностью воспроизвести, просто запустив некоторый код.
  • Разработку через тестирование сложно применять в тех случаях, когда для тестирования необходимо прохождение функциональных тестов. Примерами может быть: разработка интерфейсов пользователя, программ, работающих с базами данных, а также того, что зависит от специфической конфигурации сети. Разработка через тестирование не предполагает большого объёма работы по тестированию такого рода вещей. Она сосредотачивается на тестировании отдельно взятых модулей, используя mock-объекты для представления внешнего мира.
  • Требуется больше времени на разработку и поддержку, а одобрение со стороны руководства очень важно. Если у организации нет уверенности в том, что разработка через тестирование улучшит качество продукта, то время, потраченное на написание тестов, может рассматриваться как потраченное впустую.[9]
  • Модульные тесты, создаваемые при разработке через тестирование, обычно пишутся теми же, кто пишет тестируемый код. Если разработчик неправильно истолковал требования к приложению, и тест, и тестируемый модуль будут содержать ошибку.
  • Большое количество используемых тестов могут создать ложное ощущение надежности, приводящее к меньшему количеству действий по контролю качества.
  • Тесты сами по себе являются источником накладных расходов. Плохо написанные тесты, например, содержат жёстко вшитые строки с сообщениями об ошибках или подвержены ошибкам, дороги при поддержке. Чтобы упростить поддержку тестов следует повторно использовать сообщения об ошибках из тестируемого кода.
  • Уровень покрытия тестами, получаемый в результате разработки через тестирование, не может быть легко получен впоследствии. Исходные тесты становятся всё более ценными с течением времени. Если неудачные архитектура, дизайн или стратегия тестирования приводят к большому количеству непройденных тестов, важно их все исправить в индивидуальном порядке. Простое удаление, отключение или поспешное изменение их может привести к необнаруживаемым пробелам в покрытии тестами.

Видимость кода[править | править исходный текст]

Набор тестов должен иметь доступ к тестируемому коду. С другой стороны, принципы инкапсуляции и сокрытия данных не должны нарушаться. Поэтому модульные тесты обычно пишутся в том же модуле или проекте, что и тестируемый код.

Из кода теста может не быть доступа к частным полям и методам. Поэтому при модульном тестировании может потребоваться дополнительная работа. В Java, разработчик может использовать отражение (англ. reflection), чтобы обращаться к полям, помеченными как частные.[10] Модульные тесты можно реализовать во внутренних классах, чтобы они имели доступ к членам внешнего класса. В .NET Framework могут применяться разделяемые классы (англ. partial classes) для доступа из теста к частным полям и методам.

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

Не существует единого мнения среди программистов, применяющих разработку через тестирование, о том, насколько осмысленно тестировать частные, защищённые методы, а также данные. Одни убеждены, что достаточно протестировать любой класс только через его публичный интерфейс, поскольку частные переменные — это всего лишь деталь реализации, которая может меняться, и её изменения не должны отражаться на наборе тестов. Другие утверждают, что важные аспекты функциональности могут быть реализованы в частных методах и тестирование их неявно через публичный интерфейс лишь усложнит ситуацию: модульное тестирование предполагает тестирование наименьших возможных модулей функциональности.[11][12]

Fake-, mock-объекты и интеграционные тесты[править | править исходный текст]

Модульные тесты тестируют каждый модуль по отдельности. Не важно, содержит ли модуль сотни тестов или только пять. Тесты, используемые при разработке через тестирование, не должны пересекать границы процесса, использовать сетевые соединения. В противном случае прохождение тестов будет занимать большое время, и разработчики будут реже запускать набор тестов целиком. Введение зависимости от внешних модулей или данных также превращает модульные тесты в интеграционные. При этом если один модуль в цепочке ведет себя неправильно, может быть не сразу понятно какой именно[источник не указан 614 дней].

Когда разрабатываемый код использует базы данных, веб-сервисы или другие внешние процессы, имеет смысл выделить покрываемую тестированием часть. Это делается в два шага:

  1. Везде, где требуется доступ к внешним ресурсам, должен быть объявлен интерфейс, через который этот доступ будет осуществляться. См. принцип инверсии зависимостей (англ. dependency inversion) для обсуждения преимуществ этого подхода независимо от TDD.
  2. Интерфейс должен иметь две реализации. Первая, собственно предоставляющая доступ к ресурсу, и вторая, являющаяся fake- или mock-объектом. Всё, что делают fake-объекты, это добавляют сообщения вида «Объект person сохранен» в лог, чтобы потом проверить правильность поведения. Mock-объекты отличаются от fake- тем, что сами содержат утверждения (англ. assertion), проверяющие поведение тестируемого кода. Методы fake- и mock-объектов, возвращающие данные, можно настроить так, чтобы они возвращали при тестировании одни и те же правдоподобные данные. Они могут эмулировать ошибки так, чтобы код обработки ошибок мог быть тщательно протестирован. Другими примерами fake-служб, полезными при разработке через тестирование, могут быть: служба кодирования, которая не кодирует данные, генератор случайных чисел, который всегда выдает единицу. Fake- или mock-реализации являются примерами внедрения зависимости (англ. dependency injection).

Использование fake- и mock-объектов для представления внешнего мира приводит к тому, что настоящая база данных и другой внешний код не будут протестированы в результате процесса разработки через тестирование. Чтобы избежать ошибок, необходимы тесты реальных реализаций интерфейсов, описанных выше. Эти тесты могут быть отделены от остальных модульных тестов и реально являются интеграционными тестами. Их необходимо меньше, чем модульных, и они могут запускаться реже. Тем не менее, чаще всего они реализуются используя те же библиотеки для тестирования (англ. testing framework), что и модульные тесты.

Интеграционные тесты, которые изменяют данные в базе данных, должны откатывать состоянии базы данных к тому, которое было до запуска теста, даже если тест не прошёл. Для этого часто применяются следующие техники:

  • Метод TearDown, присутствующий в большинстве библиотек для тестирования.
  • try...catch...finally структуры обработки исключений, там где они доступны.
  • Транзакции баз данных.
  • Создание снимка (англ. snapshot) базы данных перед запуском тестов и откат к нему после окончания тестирования.
  • Сброс базы данных в чистое состояние перед тестом, а не после них. Это может быть удобно, если интересно посмотреть состояние базы данных, оставшееся после не прошедшего теста.

Существуют библиотеки Moq, jMock, NMock, EasyMock, Typemock, jMockit, Unitils, Mockito, Mockachino, PowerMock or Rhino Mocks, предназначенные упростить процесс создания mock-объектов.

См. также[править | править исходный текст]

Примечания[править | править исходный текст]

  1. 1 2 3 Beck, K. Test-Driven Development by Example, Addison Wesley, 2003
  2. Lee Copeland. Extreme Programming. Computerworld (December 2001). Проверено 11 января 2011. Архивировано из первоисточника 28 августа 2011.
  3. 1 2 Newkirk, JW and Vorontsov, AA. Test-Driven Development in Microsoft .NET, Microsoft Press, 2004
  4. Koskela, L. «Test Driven: TDD and Acceptance TDD for Java Developers», Manning Publications, 2007
  5. Erdogmus, Hakan; Morisio, Torchiano. On the Effectiveness of Test-first Approach to Programming. Proceedings of the IEEE Transactions on Software Engineering, 31(1). January 2005. (NRC 47445). — «We found that test-first students on average wrote more tests and, in turn, students who wrote more tests tended to be more productive.»  Проверено 14 января 2008. Архивировано из первоисточника 28 августа 2011.
  6. Proffitt, Jacob TDD Proven Effective! Or is it?. — «So TDD's relationship to quality is problematic at best. Its relationship to productivity is more interesting. I hope there's a follow-up study because the productivity numbers simply don't add up very well to me. There is an undeniable correlation between productivity and the number of tests, but that correlation is actually stronger in the non-TDD group (which had a single outlier compared to roughly half of the TDD group being outside the 95% band).»  Проверено 21 февраля 2008. Архивировано из первоисточника 28 августа 2011.
  7. Llopis, Noel Stepping Through the Looking Glass: Test-Driven Game Development (Part 1)(недоступная ссылка — история). Games from Within (20 February 2005). — «Comparing [TDD] to the non-test-driven development approach, you're replacing all the mental checking and debugger stepping with code that verifies that your program does exactly what you intended it to do.»  Проверено 1 ноября 2007. Архивировано из первоисточника 22 февраля 2005.
  8. Müller, Matthias M.; Padberg, Frank. About the Return on Investment of Test-Driven Development (PDF) 6. Universität Karlsruhe, Germany. Проверено 1 ноября 2007. Архивировано из первоисточника 28 августа 2011.
  9. Loughran, Steve Testing (PDF). HP Laboratories (November 6th, 2006). Проверено 12 августа 2009. Архивировано из первоисточника 28 августа 2011.
  10. Burton, Ross Subverting Java Access Protection for Unit Testing. O'Reilly Media, Inc. (11/12/2003). Проверено 12 августа 2009. Архивировано из первоисточника 28 августа 2011.
  11. Newkirk, James Testing Private Methods/Member Variables - Should you or shouldn't you. Microsoft Corporation (7 June 2004). Проверено 12 августа 2009. Архивировано из первоисточника 28 августа 2011.
  12. Stall, Tim How to Test Private and Protected methods in .NET. CodeProject (1 Mar 2005). Проверено 12 августа 2009. Архивировано из первоисточника 28 августа 2011.

Литература[править | править исходный текст]

  • Лайза Криспин, Джанет Грегори Гибкое тестирование: практическое руководство для тестировщиков ПО и гибких команд = Agile Testing: A Practical Guide for Testers and Agile Teams. — М.: «Вильямс», 2010. — 464 с. — (Addison-Wesley Signature Series). — 1000 экз. — ISBN 978-5-8459-1625-9
  • Кент Бек. Экстремальное программирование: разработка через тестирование. — «Питер», 2003. ISBN 5-8046-0051-6, ISBN 0-321-14653-0