Разработка через тестирование: различия между версиями
[отпатрулированная версия] | [отпатрулированная версия] |
Строка 101: | Строка 101: | ||
== Слабые места == |
== Слабые места == |
||
* Самым первым недостатком данного подхода является то, что к нему сложно привыкнуть ментально |
* Самым первым недостатком данного подхода является то, что к нему сложно привыкнуть ментально. |
||
* Определенно существуют задачи, которые невозможно (по крайней мере, на текущий момент) решить только при помощи тестов. В частности, TDD не позволяет механически продемонстрировать адекватность разработанного кода в области безопасности данных и взаимодействия между процессами. Безусловно, безопасность основана на коде , в котором не должно быть дефектов, однако она основана также на участии человека в процедурах защиты данных. Тонкие проблемы, возникающие в области взаимодействия между процессами, невозможно с уверенностью воспроизвести, просто запустив некоторый код. |
|||
* Разработку через тестирование сложно применять в тех случаях, когда для тестирования необходимо прохождение функциональных тестов. Примерами может быть: разработка интерфейсов пользователя, программ, работающих с базами данных, а также того, что зависит от специфической конфигурации сети. Разработка через тестирование не предполагает большого объема работы по тестированию такого рода вещей. Она сосредотачивается на тестировании отдельно взятых модулей, используя mock-объекты, чтобы представить внешний мир. |
* Разработку через тестирование сложно применять в тех случаях, когда для тестирования необходимо прохождение функциональных тестов. Примерами может быть: разработка интерфейсов пользователя, программ, работающих с базами данных, а также того, что зависит от специфической конфигурации сети. Разработка через тестирование не предполагает большого объема работы по тестированию такого рода вещей. Она сосредотачивается на тестировании отдельно взятых модулей, используя mock-объекты, чтобы представить внешний мир. |
||
* Требуется больше времени на разработку и поддержку, а поддержка со стороны руководства очень важна. Если в организации нет уверенности в том, что разработка через тестирование улучшит качество продукта, то время, потраченное на написание тестов, может рассматриваться как потраченное впустую.<ref>{{cite web |
* Требуется больше времени на разработку и поддержку, а поддержка со стороны руководства очень важна. Если в организации нет уверенности в том, что разработка через тестирование улучшит качество продукта, то время, потраченное на написание тестов, может рассматриваться как потраченное впустую.<ref>{{cite web |
Версия от 22:47, 23 ноября 2011
Разработка через тестирование (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] Этот процесс позволяет гарантировать, что приложение удовлетворяет сформулированным требованиям. При разработке через приёмочное тестирование, команда разработчиков сконцентрирована на четкой задаче: удовлетворить приемочные тесты, которые отражают соответствующие требования пользователя.
Преимущества
Исследование 2005 года показало, что использование разработки через тестирование предполагает написание большего количества тестов, в свою очередь, программисты, пишущие больше тестов, склонны быть более продуктивными.[5] Гипотезы связывающие качество кода с TDD были неубедительны.[6]
Программисты, использующие TDD на новых проектах, отмечают, что они реже ощущают необходимость использовать отладчик. Если некоторые из тестов неожиданно перестают проходить, откат к последней версии, которая проходит все тесты, может быть более продуктивным, нежели отладка.[7]
Разработка через тестирование предлагает больше, чем просто проверка корректности, она также влияет на дизайн программы. Изначально сфокусировавшись на тестах, проще представить, какая функциональность необходима пользователю. Таким образом, разработчик продумывает детали интерфейса до реализации. Контрактное программирование (Design by Contract) дополняет тестирование, формируя необходимые требования через утверждения (assertions).
Несмотря на то, что при разработке через тестирование требуется написать большее количество кода, общее время, затраченное на разработку, обычно оказывается меньше.[8] Большое количество тестов помогает уменьшить количество ошибок в коде. Устранение дефектов на более раннем этапе разработки, препятствует появлению хронических и дорогостоящих ошибок, приводящих к длительной и утомительной отладке в дальнейшем.
Разработка через тестирование способствует более модульному, гибкому и расширяемому коду. Это связано с тем, что при этой методологии разработчику необходимо думать о программе, как о множестве небольших модулей, которые написаны и протестированы независимо и лишь потом соединены вместе. Это приводит к меньшим, более специализированным классам, уменьшению связанности и более чистым интерфейсам. Использование mock-объектов также вносит вклад в модуляризацию кода, поскольку требует наличия простого механизма для переключения между mock- и обычными классами.
Поскольку пишется лишь тот код, что необходим для прохождения теста, автоматизированные тесты покрывают все пути исполнения. Например, перед добавлением нового условного оператора, разработчик должен написать тест, мотивирующий добавление этого условного оператора. В результате получившиеся в результате разработки через тестирование тесты достаточно полны: они обнаруживают любые непреднамеренные изменения поведения кода.
Слабые места
- Самым первым недостатком данного подхода является то, что к нему сложно привыкнуть ментально.
- Определенно существуют задачи, которые невозможно (по крайней мере, на текущий момент) решить только при помощи тестов. В частности, TDD не позволяет механически продемонстрировать адекватность разработанного кода в области безопасности данных и взаимодействия между процессами. Безусловно, безопасность основана на коде , в котором не должно быть дефектов, однако она основана также на участии человека в процедурах защиты данных. Тонкие проблемы, возникающие в области взаимодействия между процессами, невозможно с уверенностью воспроизвести, просто запустив некоторый код.
- Разработку через тестирование сложно применять в тех случаях, когда для тестирования необходимо прохождение функциональных тестов. Примерами может быть: разработка интерфейсов пользователя, программ, работающих с базами данных, а также того, что зависит от специфической конфигурации сети. Разработка через тестирование не предполагает большого объема работы по тестированию такого рода вещей. Она сосредотачивается на тестировании отдельно взятых модулей, используя mock-объекты, чтобы представить внешний мир.
- Требуется больше времени на разработку и поддержку, а поддержка со стороны руководства очень важна. Если в организации нет уверенности в том, что разработка через тестирование улучшит качество продукта, то время, потраченное на написание тестов, может рассматриваться как потраченное впустую.[9]
- Модульные тесты, создаваемые при разработке через тестирование, обычно пишутся теми же, кто пишет тестируемый код. Если разработчик неправильно истолкует требования к приложению, и тест, и тестируемый модуль будут содержать ошибку.
- Большое количество используемых тестов могут создать ложное ощущение надежности, приводящее к меньшему количеству действий по контролю качества.
- Тесты сами по себе являются источником накладных расходов. Плохо написанные тесты, например, те, что содержат жёстко вшитые строки с сообщениями об ошибках или же те, что подвержены ошибкам, дороги при поддержке. Чтобы упростить поддержку тестов, в данном случае, следует повторно использовать сообщения об ошибках из тестируемого кода.
- Уровень покрытия тестами, получаемый в результате разработки через тестирование, не может быть легко получен потом. Исходные тесты становятся всё более ценными с течением времени. Если плохая архитектура, плохой дизайн или плохая стратегия тестирования приводят к большому количеству непройденных тестов, важно их все исправить в индивидуальном порядке. Простое удаление, отключение или поспешное изменение их может привести к необнаруживаемым пробелам в покрытии тестами.
- Рефакторинг приводит к переписыванию тестов.
Видимость кода
Набор тестов должен иметь доступ к тестируемому коду. С другой стороны, принципы инкапсуляции и сокрытия данных не должны нарушаться. Поэтому модульные тесты обычно пишутся в том же модуле или проекте, что и тестируемый код.
Из кода теста может не быть доступа к private полям и методам. Поэтому при модульном тестировании может потребоваться дополнительная работа. В Java, разработчик может использовать отражение (reflection), чтобы обращаться к полям, помеченным как private.[10] Модульные тесты можно реализовать во внутренних классах, чтобы они имели доступ к членам внешнего класса. В .NET Framework могут применяться разделяемые классы (partial classes) для доступа из теста к private полям и методам.
Важно, чтобы фрагменты кода, предназначенные исключительно для тестирования, не оставались в выпущенном коде. В Си для этого могут быть использованы директивы условной компиляции. Однако это будет означать, что выпускаемый код не полностью совпадает с тем, с которым, модуль был протестирован. Систематический запуск интеграционных тестов на выпускаемой сборке поможет удостовериться, что не осталось кода, скрыто полагающегося на различные аспекты модульных тестов.
Не существует единого мнения среди программистов, применяющих разработку через тестирование, о том, насколько это осмысленно тестировать private- и protected-методы и данные. Одни убежденны, что достаточно протестировать любой класс только через его public-интерфейс, поскольку private-члены — это всего лишь деталь реализации, которая может меняться, и её изменения не должны отражаться на наборе тестов. Другие утверждают, что важные аспекты функциональности могут быть реализованы в private-методах и тестирование их неявно через public-интерфейс лишь усложнит ситуацию: модульное тестирование предполагает тестирование наименьших возможных модулей функциональности.[11][12]
Fake-, mock-объекты и интеграционные тесты
Модульные тесты называются модульными, потому что они тестируют каждый модуль по отдельности. Не важно, содержит модуль сотни тестов или только пять. Тесты, используемые при разработке через тестирование, не должны пересекать границы процесса, использовать сетевые соединения. Иначе прохождение тестов будет занимать большое время, и разработчики будут реже запускать набор тестов целиком. Введение зависимости от внешних модулей или данных также превращает модульные тесты в интеграционные. При этом если один модуль в цепочке ведет себя неправильно, может быть не сразу понятно какой именно.
Когда разрабатываемый код использует базы данных, веб-сервисы или другие внешние процессы, имеет смысл выделить ту часть, которая покрывается модульными тестами отдельно. Это делается в два шага:
- Везде, где требуется доступ к внешним ресурсам, должен быть объявлен интерфейс, через который этот доступ будет осуществляться. См. принцип инверсии зависимостей (dependency inversion) для обсуждения преимуществ этого подхода независимо от TDD.
- Интерфейс должен иметь две реализации. Первая, собственно предоставляющая доступ к ресурсу, и вторая, являющаяся fake- или mock-объектом. Всё, что делают fake-объекты, это добавляют сообщения вида «Объект person сохранен» в лог, чтобы потом проверить правильность поведения. Mock-объекты отличаются от fake-, тем, что сами содержат утверждения (assertion), проверяющие поведение тестируемого кода. Методы fake- и mock-объектов, возвращающие данные, можно настроить так, чтобы они возвращали при тестировании одни и те же правдоподобные данные. Они могут эмулировать ошибки так, чтобы код обработки ошибок мог быть тщательно протестирован. Другими примерами fake-служб, полезными при разработке через тестирование, могут быть: служба кодирования, которая не кодирует данные, генератор случайных чисел, который всегда выдает единицу. Fake- или mock-реализации являются примерами внедрения зависимости (dependency injection).
Использование fake- и mock-объектов для представления внешнего мира приводит к тому, что настоящая база данных и другой внешний код не будут протестированы в результате процесса разработки через тестирование. Чтобы избежать ошибок, необходимы тесты реальных реализаций интерфейсов, описанных выше. Эти тесты могут быть отделены от остальных модульных тестов и реально являются интеграционными тестами. Их необходимо меньше, чем модульных, и они могут запускаться реже. Тем не менее, чаще всего они реализуются используя те же библиотеки для тестирования (testing framework), что и модульные тесты.
Интеграционные тесты, которые изменяют данные в базе данных, должны откатывать состоянии базы данных к тому, которое было до запуска теста, даже если тест не прошёл. Для этого часто применяются следующие техники:
- Метод
TearDown
, присутствующий в большинстве библиотек для тестирования (test frameworks). try...catch...finally
структуры обработки исключений, там где они доступны.- Транзакции баз данных.
- Создание снимка (snapshot) базы данных перед запуском тестов и откат к нему после окончания тестирования.
- Сброс базы данных в чистое состояние перед тестом, а не после них. Это может быть удобно, если интересно посмотреть состояние базы данных, оставшееся после не прошедшего теста.
Существуют библиотеки Moq, jMock, NMock, EasyMock, Typemock, jMockit, Unitils, Mockito, Mockachino, PowerMock or Rhino Mocks, предназначенные упростить процесс создания mock-объектов.
Литература
- Лайза Криспин, Джанет Грегори. Гибкое тестирование: практическое руководство для тестировщиков ПО и гибких команд = 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
См. также
Источники
- ↑ 1 2 3 Beck, K. Test-Driven Development by Example, Addison Wesley, 2003
- ↑ Lee Copeland. Extreme Programming . Computerworld (декабрь 2001). Дата обращения: 11 января 2011. Архивировано 28 августа 2011 года.
- ↑ 1 2 Newkirk, JW and Vorontsov, AA. Test-Driven Development in Microsoft .NET, Microsoft Press, 2004.
- ↑ Koskela, L. "Test Driven: TDD and Acceptance TDD for Java Developers", Manning Publications, 2007
- ↑ 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 года.
- ↑ 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 года.
- ↑ Llopis, Noel Stepping Through the Looking Glass: Test-Driven Game Development (Part 1) . Games from Within (20 февраля 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.
- ↑ 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 года.
- ↑ Loughran, Steve Testing (PDF). HP Laboratories (November 6th, 2006). Дата обращения: 12 августа 2009. Архивировано 28 августа 2011 года.
- ↑ Burton, Ross Subverting Java Access Protection for Unit Testing . O'Reilly Media, Inc. (11 декабря 2003). Дата обращения: 12 августа 2009. Архивировано 28 августа 2011 года.
- ↑ Newkirk, James Testing Private Methods/Member Variables - Should you or shouldn't you . Microsoft Corporation (7 июня 2004). Дата обращения: 12 августа 2009. Архивировано 28 августа 2011 года.
- ↑ Stall, Tim How to Test Private and Protected methods in .NET . CodeProject (1 марта 2005). Дата обращения: 12 августа 2009. Архивировано 28 августа 2011 года.