Керована тестами розробка
Цикл розробки програмного забезпечення |
---|
Діяльність і кроки |
Допоміжні дисципліни |
Практики |
Інструменти |
Стандарти та галузі знань |
Керована тестами розробка (КТР), Розробка через тестування (англ. Test-driven development (TDD)) — технологія розробки програмного забезпечення, яка використовує короткі ітерації розробки, що починаються з попереднього написання тестів, які визначають необхідні покращення або нові функції. Кожна ітерація має на меті розробити код, який пройде ці тести. Нарешті, програміст або група вдосконалюють код для погодження змін. Один із ключових моментів TDD полягає у тому, що підготовка тестів перед написанням самого коду пришвидшує процес внесення змін. Варто зауважити, що керована тестами розробка є методологією розробки програмного забезпечення, а не його тестування.
Test-Driven Development відноситься до концепції екстремального програмування, яка стверджує, що спершу потрібно писати тести, а вже потім код, яка веде свій початок з 1999 року,[1] однак, останнім часом спостерігається загальніший інтерес до даної методології.[2]
Програмісти також використовують дану методологію для вдосконалення і зневадження сирцевого коду, раніше написаного з використанням інших методологій розробки.[3]
Керована тестами розробка вимагає від розробника створення автоматизованих модульних тестів, які визначають вимоги до коду безпосередньо перед написанням самого коду. Тест містить перевірки умов, які можуть або виконуватися, або ні. Коли вони виконуються, кажуть, що тест пройдено. Проходження тесту підтверджує поведінка, передбачувана програмістом. Розробники часто використовують програмні каркаси для тестування (англ. testing frameworks) для створення та автоматизації запуску наборів тестів. На практиці модульні тести покривають критичні та нетривіальні ділянки коду. Це може бути код, схильний до частих змін, код, від роботи якого залежить працездатність великої кількості іншого коду, або код з великою кількістю залежностей.
Середовище розробки повинно швидко реагувати на невеликі модифікації коду. Архітектура програми повинна базуватися на використанні безлічі сильно пов’язаних компонентів, які слабо залежать одне від одного , завдяки чому тестування коду спрощується.
TDD не тільки пропонує перевірку коректності, а й впливає на дизайн програми. Спираючись на тести, розробники можуть швидше уявити, яка функціональність необхідна користувачеві. Таким чином, деталі інтерфейсу з'являються задовго до остаточної реалізації рішення.
Зрозуміло, що до тестів застосовуються такі ж вимоги щодо якості коду, як і до основного коду.
- Не можна писати жодного вихідного коду, доки спершу не написано падаючого юніт-тесту (англ. unit test).
- Не можна писати більше юніт-тесту ніж необхідно для падіння (непроходження тесту). Помилка компіляції - це також падіння (англ. failing).
- Не можна писати більше вихідного коду, ніж необхідно для проходження вдалого юніт-тесту.
Усе нижчевказане базується на книзі Test-Driven Development by Example,[4] котру багато хто вважає канонічним текстом про дану концепцію у її сучасному вигляді.
У розробці через тестування, реалізація кожної нової функції розпочинається з написання тесту. Цей тест звісно ж не буде проходити, адже він написаний до того, як було реалізовано даний функціонал. Для того, щоб написати тест, розробник повинен чітко розуміти майбутні специфікацію та вимоги до нового функціоналу. Це основна відмінність розробки через тестування від більш класичного підходу написання тестів після того, як написано сам код: це змушує розробника фокусуватись на специфікаціях перед написанням коду.
Це підтверджує, що усі тести працюють коректно, і, що нові тести помилково не проходять, не вимагаючи при цьому ніякого нового коду.
Нові тести також не повинні проходити з відомої причини. Цей етап є тестуванням самих тестів на те, чи дають вони негативний результат: це виключає можливість того, що новий тест буде завжди проходити, а, отже, буде марним.
Наступним кроком є написання коду, який змусить тест пройти. Новий код, написаний на даному етапі не буде досконалим, і може, наприклад, змушувати тест проходити у некоректний спосіб. Це є допустимим. тому що на наступних кроках даний код буде поліпшуватись і відточуватись.
Дуже важливим моментом є те, що код написаний виключно для того, щоб проходили тести; ніякої додаткової (а отже і не протестованої) функціональності на даному етапі вносити не дозволяється.
Якщо усі тести успішно проходять, програміст може бути впевненим у тому, що його код відповідає усім вимогам, які перевіряються тестами. Це гарна точка, з якої можна перейти на фінальний етап циклу розробки.
Тепер код при необхідності може бути "вичищений". Шляхом перезапуску усіх тестів, розробник може впевнитись у тому, що рефакторинг коду не порушив його відповідність специфікаціям. Концепція вилучення з коду дублікатів є важливим аспектом будь-якого дизайну програмного забезпечення.
Керована тестами розробка тісно пов'язана з такими принципами як «роби простіше, дурник» (англ. keep it simple, stupid, KISS) і «вам це не знадобиться» (англ. you ain't gonna need it, YAGNI). Дизайн може бути чистіше і ясніше при написанні лише того коду, який необхідний для проходження тесту. [4] Кент Бек також пропонує принцип «підроби, поки не зробиш» (англ. fake it till you make it).
Тести повинні писатися для тестованої функціональності. Вважається, що це має дві переваги. Це допомагає переконатися, що застосунок придатний для тестування, оскільки розробнику доведеться з самого початку обдумати те, як застосунок буде тестуватися. Це також сприяє тому, що тестами буде покрита вся функціональність. Коли функціональність пишеться до тестів, розробники та організації схильні переходити до реалізації наступної функціональності, ще не протестувавши існуючу.
Ідея перевіряти, що тільки написаний тест не проходить, допомагає переконатися, що тест реально щось перевіряє. Тільки після цієї перевірки слід приступати до реалізації нової функціональності. Цей прийом, відомий як «червоний / зелений / рефакторинг», називають «мантрою розробки через тестування». Під червоним тут розуміють тести, які не пройшли, а під зеленим - які пройшли тестування.
Відпрацьовані практики розробки через тестування призвели до створення техніки «Керована приймальними тестами розробка» (англ. Acceptance Test-driven development, ATDD), в якому критерії описані замовником автоматизуються в приймальні тести, що використовуються потім в звичайному процесі керованої модульними тестами розробки (англ. unit test-driven development, UTDD). [5] Цей процес дозволяє гарантувати, що застосунок задовольняє сформульованим вимогам. При керованій приймальними тестами розробці, команда розробників сконцентрована на чіткій задачі: задовольнити приймальні тести, які відображають відповідні вимоги користувача.
Приймальні (функціональні) тести (англ. customer tests, acceptance tests) - тести, що перевіряють функціональність програми на відповідність вимогам замовника. Приймальні тести проходять на стороні замовника. Це допомагає йому бути впевненим у тому, що він отримає всю необхідну функціональність.
Набір тестів повинен мати доступ до коду що тестується. З іншого боку, принципи інкапсуляції та приховування даних не повинні порушуватися. Тому модульні тести зазвичай пишуться в тому ж модулі або проекті, що і тестований код.
З коду тесту може не бути доступу до private полів і методів. Тому при модульному тестуванні може знадобитися додаткова робота. В Java, розробник може використовувати відображення (англ. reflection), щоб звертатися до полів, позначеним як private. [6] Модульні тести можна реалізувати у внутрішніх класах, щоб вони мали доступ до членів зовнішнього класу. В . NET Framework можуть застосовуватися колективні класи (англ. partial classes) для доступу з теста до private полів і методів.
Важливо, щоб фрагменти коду, призначені виключно для тестування, не залишалися у випущеному коді. В Сі для цього можуть бути використані директиви умовної компіляції. Однак це означатиме, що код, який випускається, не повністю збігається з протестованим.
Не існує єдиної думки серед програмістів, які застосовують розробку через тестування, про те, наскільки це осмислено тестувати private- і protected- методи і дані. Одні переконані, що достатньо протестувати будь-який клас тільки через його public-інтерфейс, оскільки private-члени - це всього лише деталь реалізації, яка може змінюватися, і її зміни не повинні відображатися на наборі тестів. Інші стверджують, що важливі аспекти функціональності можуть бути реалізовані в private-методах і тестування їх неявно через public-інтерфейс лише ускладнить ситуацію: модульне тестування передбачає тестування найменших можливих модулів функціональності. [7][8]
Згідно концепції TDD, тести "ведуть" розробника до певної реалізації. Проходження впалого тесту досягається завдяки трансформації вихідного коду. Часом, можна трансформувати код таким чином, що подальше тестування буде вкрай ускладнене і вимагатиме переосмислення реалізації значної частини або, навіть, усього юніта (функції, методу класу). Тобто при невірному використанні TDD може завести недосвідченого розробника в "глухий кут", або привести до неоптимальної реалізації алгоритму. Для допомоги розробникам було запропоновано принцип пріоритету трансформацій. Він полягає в тому, що потрібно завжди віддавати перевагу простішим трансформаціям над складнішими. Дотримання цього принципу може допомогти уникати "глухих кутів" та розробляти оптимальніші алгоритми виконання.
Модульні тести тестують кожен модуль окремо. Не важливо, чи містить модуль сотні тестів або тільки п'ять. Тести, які використовуються при розробці через тестування, не повинні перетинати кордони процесу, використовувати мережеві з'єднання. В іншому випадку проходження тестів буде займати великий час, і розробники будуть рідше запускати набір тестів цілком. Введення залежності від зовнішніх модулів або даних також перетворює модульні тести в інтеграційні. При цьому якщо один модуль в ланцюжку веде себе неправильно, може бути не відразу зрозуміло який саме.
Коли код що розробляється використовує бази даних, вебсервіси або інші зовнішні процеси, має сенс виділити частину тестування, що покривається. Це робиться в два кроки:
- Скрізь, де вимагається доступ до зовнішніх ресурсів, повинен бути оголошений інтерфейс, через який цей доступ буде здійснюватися.
- Інтерфейс повинен мати дві реалізації. Перша надає доступ до ресурсу, і друга, що є 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-об'єктів.
Дослідження 2005 року показало, що використання розробки через тестування передбачає написання більшої кількості тестів; у свою чергу, програмісти, які пишуть більше тестів, схильні бути більш продуктивними. [9] Гіпотези, які зв'язують якість коду з TDD були непереконливими.[10]
Програмісти, які використовують TDD на нових проектах, відзначають, що вони рідше відчувають необхідність використовувати зневаджувач. Якщо деякі з тестів несподівано перестають проходити, відкат до останньої версії, яка проходить всі тести, може бути продуктивнішим, ніж зневадження.[11]
Керована тестами розробка пропонує більше, ніж просто перевірку коректності, вона також впливає на дизайн програми. З самого початку сфокусувавшись на тестах, простіше уявити, яка функціональність необхідна користувачеві. Таким чином, розробник продумує деталі інтерфейсу до реалізації. Тести змушують робити свій код більш пристосованим для тестування. Наприклад, відмовлятися від глобальних змінних, одиничних предметів (singletons), робити класи менш пов'язаними і легкими для використання. Сильно пов'язаний код або код, який вимагає складної ініціалізації, буде значно важче протестувати. Модульне тестування сприяє формуванню чітких і невеликих інтерфейсів. Кожен клас буде виконувати певну роль, як правило невелику. Як наслідок - залежності між класами будуть зменшуватися, а зачеплення підвищуватися. Контрактне програмування (англ. design by contract) доповнює тестування, формуючи необхідні вимоги через затвердження (англ. assertions).
Незважаючи на те, що при розробці через тестування потрібно написати більшу кількість коду, загальний час, витрачений на розробку, зазвичай виявляється менше. Тести захищають від помилок. Тому час, що витрачається на зневадження, зменшується в рази. [12] Велика кількість тестів допомагає зменшити кількість помилок в коді. Усунення дефектів на більш ранньому етапі розробки перешкоджає появі хронічних помилок, що призводять до тривалого та виснажливого зневадження в майбутньому.
Тести дозволяють робити рефакторинг коду без ризику його зламати. При внесенні змін в добре протестований код, ризик появи нових помилок значно нижче. Якщо нова функціональність призводить до помилок, тести, якщо вони звичайно є, одразу ж це покажуть. При роботі з кодом, на якому немає тестів, помилку можна виявити через значний час, коли з кодом працювати буде набагато складніше. Добре протестований код легко переносить рефакторінг. Впевненість у тому, що зміни не зламають існуючу функціональність, надає впевненість розробникам і збільшує ефективність їх роботи. Якщо існуючий код добре покритий тестами, розробники будуть відчувати себе набагато вільніше при внесенні архітектурних рішень, які покликані поліпшити дизайн коду.
Керована тестами розробка сприяє більш модульному і гнучкому коду. Це пов'язано з тим, що при цій методології розробнику необхідно думати про програму, як про безліч невеликих модулів, які написані і протестовані незалежно і лише потім з'єднані разом. Це призводить до менших, більш спеціалізованих класів, зменшенню пов'язаності і чистіших інтерфейсів. Використання mock-об'єктів також вносить вклад в модулярізацію коду, оскільки вимагає наявності простого механізму для перемикання між mock- і звичайними класами.
Оскільки пишеться лише той код, що необхідний для проходження тесту, автоматизовані тести покривають всі шляхи виконання. Наприклад, перед додаванням нового умовного оператора, розробник повинен написати тест, мотивуючий додавання цього умовного оператора.
Тести можуть використовуватися як документація. Хороший код розповість про те, як він працює, краще за будь-яку документацію. Документація і коментарі в коді можуть застарівати. Це може збивати з пантелику розробників, які вивчають код. А так як документація, на відміну від тестів, не може сказати, що вона застаріла, такі ситуації, коли документація не відповідає дійсності - не рідкість.
- Головним недоліком TDD є те, що до нього тяжко звикнути.
- Існують завдання, які неможливо (принаймні, на поточний момент) вирішити тільки за допомогою тестів. Зокрема, TDD не дозволяє механічно продемонструвати адекватність розробленого коду в області безпеки даних і взаємодії між процесами. Безумовно, безпека заснована на коді, в якому не повинно бути дефектів, проте вона заснована також на участі людини у процедурах захисту даних. Тонкі проблеми, що виникають у сфері взаємодії між процесами, неможливо з упевненістю відтворити, просто запустивши деякий код.
- Розробку через тестування складно застосовувати в тих випадках, коли для тестування необхідно проходження функціональних тестів. Прикладами може бути: розробка інтерфейсів користувача, програм, що працюють з базами даних, а також того, що залежить від специфічної конфігурації мережі. Керована тестами розробка не передбачає великого обсягу роботи з тестування такого роду речей. Вона зосереджується на тестуванні окремо взятих модулів, використовуючи mock-об'єкти для представлення зовнішнього світу.
- Потрібно більше часу на розробку і підтримку, а схвалення з боку керівництва дуже важливо. Якщо в організації немає впевненості в тому, що керована тестами розробка поліпшить якість продукту, то час, витрачений на написання тестів, може розглядатися як витрачений даремно. [13]
- Модульні тести, створювані при розробці через тестування, звичайно пишуться тими ж, хто пише тестований код. Якщо розробник неправильно витлумачив вимоги до застосунку, і тест, і тестований модуль будуть містити помилку.
- Велика кількість використовуваних тестів можуть створити помилкове відчуття надійності, що призводить до меншої кількості дій з контролю якості.
- Тести самі по собі є джерелом накладних витрат. Погано написані тести, наприклад, містять жорстко вшиті рядки з повідомленнями про помилки або схильні до помилок. Щоб спростити підтримку тестів слід повторно використовувати повідомлення про помилки з тестованого коду.
- Рівень покриття тестами, що отримується в результаті розробки через тестування, не може бути легко отриманий згодом. Вихідні тести стають все більш цінними з плином часу. Якщо невдалі архітектура, дизайн або стратегія тестування призводять до великої кількості не пройдених тестів, важливо їх всі виправити в індивідуальному порядку. Просте видалення, відключення або поспішна зміна їх може призвести до прогалин у покритті тестами.
- ↑ "Extreme Programming", Computerworld (online), December 2001, webpage: Computerworld-appdev-92 [Архівовано 2011-06-05 у Wayback Machine.].
- ↑ Newkirk, JW and Vorontsov, AA. Test-Driven Development in Microsoft .NET, Microsoft Press, 2004.
- ↑ Feathers, M. Working Effectively with Legacy Code, Prentice Hall, 2004
- ↑ а б Beck, K. Test-Driven Development by Example, Addison Wesley, 2003
- ↑ Koskela, L. «Test Driven: TDD and Acceptance TDD for Java Developers», Manning Publications, 2007
- ↑ Burton, Ross (11/12/2003). Subverting Java Access Protection for Unit Testing. O'Reilly Media, Inc. Архів оригіналу за 27 липня 2009. Процитовано 12 серпня 2009.
- ↑ Newkirk, James (7 червня 2004). Testing Private Methods/Member Variables - Should you or shouldn't you. Microsoft Corporation. Архів оригіналу за 30 червня 2009. Процитовано 12 серпня 2009.
- ↑ Stall, Tim (1 березня 2005). How to Test Private and Protected methods in .NET. CodeProject. Архів оригіналу за 3 вересня 2009. Процитовано 12 серпня 2009.
- ↑ 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). Архів оригіналу за 27 серпня 2011. Процитовано 14 січня 2008.
We found that test-first students on average wrote more tests and, in turn, students who wrote more tests tended to be more productive.
- ↑
Proffitt, Jacob. TDD Proven Effective! Or is it?. Архів оригіналу за 27 серпня 2011. Процитовано 21 лютого 2008.
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).
- ↑ Llopis, Noel (20 лютого 2005). Stepping Through the Looking Glass: Test-Driven Game Development (Part 1). Games from Within. Архів оригіналу за 22 лютого 2005. Процитовано 1 листопада 2007.
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.
- ↑ Müller, Matthias M.; Padberg, Frank. About the Return on Investment of Test-Driven Development (PDF). Universität Karlsruhe, Germany. с. 6. Архів оригіналу (PDF) за 11 червня 2007. Процитовано 1 листопада 2007.
- ↑ Loughran, Steve (November 6th, 2006). Testing (PDF). HP Laboratories. Архів оригіналу (PDF) за 20 лютого 2009. Процитовано 12 серпня 2009.