Тема автоматизованих тестів – це ще одна популярна тема, про яку часто питають початківці. Найчастіше запитують:
- що таке ці тести і якими вони бувають?
- коли потрібно ними займатись в проекті?
- що таке TDD, Continuous Integration, Test Coverage?
- як зробити тести максимально корисними і ефективними?
У своїй книзі “Веб-розробка з Python та Django для Початківців” я присвятив цілу главу автоматизованим тестам. В даній главі є доволі об’ємна теоретична частина, яка вводить студента у курс справи і відповідає на усі вищенаведені запитання.
Тому у сьогоднішній статті я просто викладаю теоретичний вступ 14-ї глави своєї книги…
А далі прямий текст із книги:
***
Вступ
Існує велика кількість типів тестів. Відповідно до типу тести можуть мати різне призначення:
- довести, що код проекту працює коректно;
- вказати на наявну помилку в коді програми;
- перевірити правильність конфігурації системи у різних середовищах (демонстраційне, розробницьке, продакшн); • перевірити безпеку програми;
- показати, що програма готова до відповідних навантажень з боку користувачів;
- перевірити, що функціонал аплікації є легким у користуванні;
- допомогти спроектувати структуру та дизайн проекту.
Таким чином, тест – це автоматизована (тест виконує код) чи ручна (тест виконує людина) дія, яка, використовуючи відповідний набір інструментів, виконує одне із вищенаведених завдань. Найбільш поширеним завданням є перевірка коду програми на коректність і відсутність помилок.
В даній главі ми зосередимось саме на автоматизованих тестах, які перевіряють коректність раніше написаного нами коду веб-аплікації. Усі інші типи тестів ми зачіпати не будемо, і вони залишаються поза даною главою.
Цього вам буде абсолютно достатньо, щоб пройти співбесіду. Також, якщо ви уже програмуєте, але ще не знайомі із створенням автоматичних тестів, саме ці тести (на перевірку коректності коду) є основними тестами, з яких варто почати знайомство із наукою тестування для програміста.
Типи тестів
Існує велика кількість критеріїв для розподілу тестів на типи. Кожен із типів має свій ідеальний час та причину для створення. Давайте коротенько пройдемось по категоріях. В кожній категорії наведемо мету, час для написання в циклі проекту та причину створення даного типу тесту.
По об’єкту тестування:
- функціональне тестування: тести на функціонал програми; створюються до, протягом або після написання коду програми; мають зміст у довготривалих проектах для підвищення якості коду та стабільності роботи аплікації;
- тестування продуктивності: сюди входять тести по навантаженню програми, стрес-тестування та тестування стабільності програми; дані тести необхідні для гарантії достатньої продуктивності роботи програми при заданій кількості користувачів; дані тести не варто писати на початковому етапі життя аплікації, коли користувачів мало; зазвичай, дані тести створюються пізніше, якщо кількість користувачів різко збільшується;
- конфігураційне тестування: дані тести допомагають перевірити правильність налаштування системи у різних середовищах; актуальні при розробці програми, яка використовується на різних платформах, середовищах та пристроях;
- юзабіліті тестування: з англ. usability – ергономічність, зручність; ці тести перевіряють функціонал на зручність для користувача; зазвичай, дані тести проводяться з допомогою залучення реальних користувачів в якості тестувальників;
- тестування інтерфейсу користувача: тест, що перевіряє чи користувацький інтерфейс має запланований вигляд; наприклад, у веб-розробці це будуть тести, які перевірятимуть розміри та розташування елементів на веб-сторінці; так, на даний момент існують інструменти, що дозволяють тестувати навіть верстку у вебі;
- тестування безпеки: тести для оцінки вразливості системи до різного роду атак; створюються в особливо критичних частинах програм (платіжна система, програма бек-офіс банку, і т.д.), а також при виявленні дірки в безпеці;
- тестування локалізації: тести, які перевіряють переклад програми на одну із мов; дані тести доцільно проводити в багатомовних програмах при кожному оновленні перекладів;
- тестування сумісності: перевірка окремих компонент програми на сумісність між собою; також перевірка зовнішнього програмного забезпечення для коректної роботи програми в їхньому середовищі.
По доступу до системи:
- тестування чорного ящика: тестування програми з точки зору кінцевого користувача, при якому не використовуються знання про внутрішню будову програми;
- тестування білого ящика: тестування самого коду з метою перевірки коректності роботи програми; зазвичай, методами тієї мови, на якій писалась сама програма; більшість тестів, які ми створюватимемо в даній главі будуть саме тестами білого ящика;
- тестування сірого ящика: це тестування чорного ящика, при якому відома внутрішня сторона програми; такі знання допомагають краще організувати самі тести і покривати більшу область можливих дефектів у програмі.
По рівню автоматизації:
- ручне тестування: тести, які проводять тестувальники без використання програмних засобів;
- автоматизоване тестування: дані тести створюються один раз і далі використовуються при кожній потребі в процесі життя програми; дані тести використовують програмні засоби для виконання тестів і перевірки результатів;
- напів-автоматизоване тестування: в даному випадку частина тесту відбувається вручну, а інша частина з допомогою наперед підготованого коду.
По рівню ізольованості компонентів:
- модульне тестування: також називається юніт-тестуванням (з англ. unit – модуль, одиниця); тести, що дозволяють перевірити на коректність окрему одиницю коду (функцію, клас і т.д.); при цьому тести пишуться так, щоб предмет тесту був максимально незалежним від інших компонентів системи;
- інтеграційне тестування: тестування модулів, об’єднаних в групи; більшість тестів у даній главі будуть саме інтеграційними, адже у фреймворку Django більшість компонентів зав’язані на базу даних і цілу низку інших аспектів, які унеможливлюють тестування окремих модулів коду;
- системне тестування: тестування програми як цілісної системи включно з платформою, на якій її запускають; мета такого тестування – виявити усі помилки, що стосуються як функціоналу програми, так і конфліктів сумісності із кінцевою платформою запуску програми.
По періоду запуску тестів:
- альфа-тестування: проводиться на ранньому етапі розробки програми; зазвичай проводять потенційні користувачі або замовники;
- бета-тестування: проводиться на кінцевому етапі розробки програми; зазвичай проводить невелика група реальних користувачів.
Багато типів тестів перетинаються між собою у вищенаведених категоріях. Так, наприклад, тестування білого та чорного ящика зазвичай є функціональними тестами та системними тестами. Тест чорного ящика можна проводити як вручну, так і з допомогою коду.
В даній главі ми працюватимемо із автоматизованими тестами. Тестування проводитиметься здебільшого у вигляді тестування білого та сірого ящиків. Буде лише кілька юніт-тестів, а решта належатиме до тестів інтеграційних в силу того, що ми працюємо всередині фреймворку, а не пишемо код з нуля. Усі ці тести можна назвати функціональними тестами. Також будуть один-два тести на безпеку, а саме на перевірку доступу для аноніма до кількох в’юшок нашої аплікації. Інших типів тестів (продуктивність, навантаженість, конфігурація і т.д.) ми не розглядатимемо в даній главі.
Писати тести чи ні?
Тепер, коли знаємо, що тести бувають різних типів і, відповідно, їхня мета створення відрізняється, можемо розібратись, як визначати чи потрібні нам тести в програмі чи ні?
Більшість типів тестів (такі як тести на продуктивність, зручність у користуванні, конфігураційних, на сумісність і т.д.), крім функціональних тестів, не варто писати одразу на старті проекту. Потреба у них з’явиться згодом. Велика частина проектів, де ви будете задіяні, взагалі не потребуватиме подібного роду тестів.
В даній главі ми писатимемо функціональні тести. Питання про доцільність тестів даного типу ми також розглянемо детальніше в даній секції.
Серед розробників є ревні прихильники тестів, які стверджують, що тести потрібно писати завжди і покривати тестами варто максимальну кількість коду в проекті. Також є і ті, хто ніколи не приділяє часу автоматичним тестам.
Я, як завжди, рекомендую шукати золоту середину між двома таборами. Кожен проект варто розглядати окремо на предмет наступних питань:
- Писати тести чи ні?
- Коли саме починати роботу над ними?
- Як багато варто додавати тестів у проект?
А взагалі, для чого нам потрібні функціональні тести?
Автоматизовані функціональні тести перевіряють код програми на коректну роботу. Ключове слово “автоматизовані” означає, що розробник чи адміністратор може сам проганяти їх, коли це необхідно. Без допомоги ззовні від тестера чи будь-кого іншого.
Таким чином, функціональні тести дозволяють швидко перевірити, чи новий код розробника працює та чи не поламав старий функціонал. Також вони дозволяють зробити швидкий тест перед тим, як закидати новий реліз програми на кінцевий сервер. Тести допомагають швидше виявити винуватця помилки по історії комітів в репозиторії коду. Тести допомагають уникати очевидних людських помилок. Функціональні тести підвищують загальний рівень якості коду.
Також тести служать документацією для складного коду і часто допомагають програмісту розібратись у чужому коді. Крім того, при рефакторингу існуючого коду можна легко виявити і відлагодити поламаний раніше працюючий код. Функціональні тести також дозволяють швидко протестувати програму на різних платформах і версіях середовища.
Після усіх вищеперечислених плюсів функціональних автоматизованих тестів складається враження, що їх варто писати завжди. Проте, після того, як я почав працювати напряму із клієнтами та обговорювати бізнес цілі проекту, а не лише його технічну сторону, почав розуміти, що тести – це не лише користь, але й додатковий бюджет для замовника.
Автоматизовані тести – це завжди код, на який треба витратити додатковий час. Часом навіть 30-50% від загального часу проекту, в залежності від його складності та кількості задіяних зовнішніх компонент та сервісів. Відповідно, на цей процент зростає вартість проекту. Крім того, тести – це річ, яка потребує часу на подальшу підтримку та оновлення відповідно до змін специфікацій проекту.
Так! Тести – це інвестиція в майбутнє проекту. З цим не можна не погодитись. Саме з цього місця і постають наступні запитання:
- Якого роду проект? Разовий чи довготерміновий?
- Яке можливе майбутнє проекту? Невизначене і з’ясується по ходу справи? А чи він точно буде затребуваним у найближчі кілька років?
В залежності від відповідей на вищенаведені запитання потреба в тестах буде різною. Ось кілька правил, якими я керуюсь, коли рекомендую або ж, навпаки, не раджу працювати над тестами в проекті:
- якщо проект невеликий і разовий (тобто подальші розробки будуть мінімальними і не буде глобального рефакторингу коду), тоді можна обійтись без тестів; в такому проекті вони будуть не інвестицією, а витратою часу та коштів;
- якщо великий і планується, довготривалий проект пишеться з нуля та при цьому вся бізнес модель (ідея) не протестована, тоді проект, швидше за все, потребуватиме частих та кардинальних експериментів у функціоналі; тоді тести не варто писати спочатку; лише на фазі, коли бізнес модель запрацювала і вже є достатньо даних, щоб зафіксувати основні специфікації функціоналу та покрити їх тестами; в протилежному випадку, якщо одразу почати писати тести в такому проекті, тести можуть переписуватись з нуля (так само як і код самого проекту) по кілька разів;
- в попередньому випадку (великий і довготривалий проект), якщо проект має хороше фінансування (із запасом) і точно потребуватиме неодноразового рефакторингу, варто одразу писати тести, але лише тести чорного ящика; в такому випадку, навіть повністю змінивши код проекту, розробник матиме швидший доказ того, що кінцевий функціонал не постраждав;
- якщо проект реалізується вже під тестовану бізнес ідею, яка працює, і є чітко визначені потреби, що не змінюватимуться, тоді тести також можна сміливо писати одразу; в майбутньому вони лише повертатимуть дивіденди у вигляді зекономлених годин на баг-фікси.
Ну і взагалі, ваша справа, як технічного спеціаліста, надати вищій ланці проекту усі плюси та мінуси, “за” і “проти” тестів саме в рамках даного проекту. І вже або ви разом, або вища ланка самостійно вирішує доцільність тестів. Головне, щоб в кінці кінців тести виявились у колонці інвестицій, а не в колонці витрат.
***
Тепер по періоду написання тестів в житті проекту. Є кілька варіантів, коли варто починати роботу над тестами:
- тести пишемо ще до написання самого коду програми; так зване TDD (Test Driven Development, детальне пояснення у наступній секції);
- тести пишемо паралельно до написання нового коду;
- тести пишемо наприкінці проекту, вже коли узгоджені кінцеві специфікації, які могли змінюватись в процесі розробки;
- є ще такий тип тесту, як тест на знайдену помилку; відповідно, його додають, якщо в існуючому функціоналі програми на кінцевому середовищі (сервері) знайдено помилку.
Загалом, краще наповнювати проект тестами раніше, ніж пізніше. Але, знову ж таки, все залежить від бізнес цілей проекту і від запитань, наведених вище про доцільність написання тестів.
Варіант написання тестів до написання самого коду ми розглянемо пізніше. Він має свої плюси та мінуси. Він не особливо відрізняється від написання тестів паралельно до написання коду з точки зору бюджету проекту.
А от написання тестів лише після тесту бізнес ідеї і затвердження остаточного функціоналу проекту в першому релізі може мати свій великий плюс: економія часу на кількаразове переписування існуючих тестів.
Підсумовуємо: в проекті із точними кінцевими специфікаціями можна одразу приступати до створення тестів. В іншому випадку, краще зекономити трохи часу і коштів та додати тести після того, як функціонал програми “устаканиться”.
Якими повинні бути функціональні тести?
При написанні тестів варто пам’ятати кілька важливих моментів, які допоможуть вам зберегти трохи часу та підвищити їхню якість:
- краще, коли один тест тестує лише одну річ (функцію, клас, блок коду);
- 100% покриття коду тестами не гарантує відсутності помилок;
- з попереднього пункту випливає, що більше число тестів не означає кращий результат; краще написати кілька тестів, що покриють критичний функціонал програми;
- також з певного моменту збільшення кількості тестів стає малоефективним; тому варто в проекті додати лише найбільш необхідні тести; 20% тестів дозволять виявити 80% помилок;
- таким чином, не варто намагатись покривати увесь код і кожний умовний оператор тестами; краще використайте цей час на розробку нового функціоналу.
***
Наприкінці теоретичної частини даної глави розглянемо ще кілька популярних англомовних абревіатур і слів у світі тестування:
TDD, Continuous Integration, Code Test Coverage
В цій теоретичній секції розберемось із трьома термінами, які наведені вище у заголовку. Останнім із них (Test Coverage) ми навіть скористаємось наприкінці даної глави.
TDD
Test Driven Development (TDD) – розробка програмного забезпечення через написання тестів.
При такому підході програміст спочатку пише тест на не існуючий функціонал чи зміну до існуючого функціоналу. Спочатку тест має провалюватись. Далі програміст додає відповідний код і після цього написані ним тести проходять успішно.
Переваги TDD підходу:
- в результаті TDD підходу отримуємо дійсно якісні тести, а не просто галочку про їх наявність;
- TDD дозволяє краще продумати архітектуру програми ще на етапі написання тестів.
Недоліки TDD підходу:
- для початківця такий підхід забере більше часу, ніж написання тестів на вже існуючий код;
- при зміні специфікацій проекту прийдеться витратити додатковий час на оновлення тестів також.
TDD підхід можна і варто використовувати у всіх проектах, в яких було вирішено писати код одразу з тестами. Даний підхід підвищить якість коду проекту і зменшить кількість помилок. Звісно, в деяких випадках TDD може забирати ще більше часу, ніж написання тестів після написання коду. Тут, знову ж таки, варто обирати підхід відповідно до бізнес-цілей проекту, його бюджету, часових рамок, а також рівня компетентності програмістів.
Continuous Integration
Continuous Integration (з англ. безперервна інтеграція) – це практика розробки програмного забезпечення, при якій проводяться регулярні збірки коду проекту з метою виявлення помилок інтеграції.
Дана практика полягає в автоматизації наступних процесів:
- отримання коду проекту з репозиторія;
- збірка проекту на окремому сервері, в середовищі максимально наближеному до кінцевого;
- запуск тестів;
- розгортання готового проекту;
- та підготовка звітів у тому чи іншому вигляді.
Вище описана процедура може виконуватись, наприклад, раз на добу. Або при кожному пуші (коміті) розробника на віддалений сервер. Таким чином, можна легко побачити, що після коміту одного із членів розробницької команди тести програми поламались, швидко виявити дану проблему та виправити.
Тобто можливість постійно моніторити стан системи на наявність помилок і є основною задачею практики Безперервної Інтеграції (Continuous Integration).
На практиці, щоб реалізувати даний підхід, використовують один із існуючих інструментів. Ось невеликий список одних із найпопулярніших опенсорсних систем Безперервної Інтеграції: Hudson, Jenkins, CruiseControl, CruiseControl.NET.
Ну і, звичайно, код проекту обов’язково повинен знаходитись в репозиторії коду, щоб була можливість реалізувати дану практику.
Code Test Coverage
Code Test Coverage – рівень покриття коду програми тестами. Код вважається покритим, якщо він був запущеним протягом прогонки тестів.
Покриття коду тестами прийнято вважати показником якості коду проекту. Хоча це не можна вважати однозначною характеристикою, адже 100% покриття коду тестами ще не означає, що код не містить помилок. До того ж кількість тестів ще не означає, що самі тести є якісні.
Відповідно, не варто намагатись покривати ваш код на всі 100% тестами. Зазвичай, достатньо написати кілька тестів, що покриватимуть лише найбільш критичні місця програми.
Якщо розглядати Django проект, то достатньо факту, що кожна в’юшка, форма і інші кастомна компоненти були запущені в результаті прогонки тестів. І не обов’язково домагатись варіанту, коли ваш тест змусить інтерпретатор увійти у кожен умовний оператор в коді. Результат від затрачених годин на такі тести є мізерним.
Django спільнота пропонує свої інструменти щодо перевірки покриття коду проекту тестами. Ми скористаємось одним із них наприкінці даної глави.
***
Сподіваюсь, ви знайшли дану інформацію щодо тестів в IT-проектах корисною і вона допоможе краще реалізувати тести у ваших щоденних справах. А головне, допоможе вирішити, коли писати тести, а коли варто від них відмовлятись.
Якщо ви уже професійно програмуєте, тоді мені цікава ваша точка зору: як ви вирішуєте, коли в проекті варто вводити автоматизовані тести?
Не писати тести — напевно, найбільш поширена причина фейлу проекту. А не писати тести на Django-проекті… не знаю, мені взагалі складно уявити собі кейс, де це було б виправдано.
100% вашого коду працює відразу як ви його написали і в ньому ніколи-ніколи нема помилок? Не обманюйте себе. 🙂 Отже, ви у будь якому разі _перевіряєте_, чи ваш код працює коректно, так? А зробити це можна, загалом, двома способами.
Спосіб перший (неправильний) — запустити проект вручну, загнати його у такий стан, при якому буде виконаним написаний вами фрагмент коду. Переконатися що він відпрацював як треба, якщо щось не так — виправити виявлені помилки, запустити ще раз, і т.д. і т.п. Важливий момент — в такому сценарії ви зазвичай перевіряєте тільки свіжий код, і в душі молитеся, щоби він не мав впливу на інші частини проекту…
Спосіб другий (правильний) — написати тест, котрий виконає ваш свіжий код. Не впевнений, чи ті, хто не пише тестів, повірять мені на слово — але майже завжди це _швидше_ і _простіше_, ніж запуск проекту вручну і тикання в UI для виклику потрібного фрагменту. Але це насправді так.
Інші переваги:
1. Завдяки тому, що Django має супер-зручний механізм для юніт-тестів, у кожному тесткейсі ви маєте _чисту_ базу даних, і по ходу виконання тестів у ній створюється саме те (і тільки те) що вам потрібно. Ніяких побічних ефектів.
2. Поступово покриваючи код тестами, ви швидко і легко виявлятимете помилки, котрі б попили чимало вашої крові без тестів. Бо дуже часто, міняючи код в одному місці проекту, можна мати неочікувану поведінку у зовсім іншому місці проекту.
3. Тести якоюсь мірою є доповненням до документації по проекту — проглянувши тести, можна досить легко зрозуміти, як працюють різні компоненти проекту і т.п.
4. Чи ви чули слово «рефакторинг»? Так от, воно «аплікабл» ТІЛЬКИ ДЛЯ КОДУ, ПОКРИТОГО ТЕСТАМИ. Якщо код не має тестів, то це не рефакторинг, а повторне переписування (з ручним перетестовуванням і тижнями відловлювання свіжих багів). Натомість, якщо ваш код покритий тестами — це дозволяє вам легко і просто вносити дуже і дуже глибокі зміни у код, не переживаючи про те, що на фініші получиться гірше, ніж було. Неочевидний наслідок — у проектах без тестів (оскільки всі бояться там взагалі будь що міняти без гострої потреби) код не рефакториться, а «протухає», обвішуючись заплатками і костилями.
Сподіваюся, я був переконливим. 🙂
І ще одне… Написання тестів, звісно, також викликає певний біль. Тут надзвичайно цікавий нюанс — що гіршим є код, тим більше болю, бо код, що має значну зв’язність, код у котрому логіка реалізована «простинями» на 2-3 екрани, важко покривати тестами.
Таким чином, якщо ви пишете тести — це м’яко і ненав’зливо, цілком «органічно», спонукає вас рухатися в сторону правильної архітектури (SOLID і ось це от все).
У будь якому випадку, навіть якщо ви починаєте писати тести, отримавши у спадок смердючий код, після його покриття монструозними тестами — у вас з’явиться можливість привести код до порядку. А у процесі приведення коду до порядку — ви матимете змогу створити гарні, компактні, максимально «зфокусовані» тести. Після чого великі а-ля інтеграційні тести можна спокійно викинути. Або не викинути, на ваш розсуд. 🙂
Если Виталий не против, могу дать ссылку не реальные (учебные) тесты, включая матрицу тестов
Лаконічно, чітко і зрозуміло. Все розкладено по поличках. Дякую.
Шкода, правда, що без прикладу 🙁