Введення в культуру DevOps: вибираємо стратегію тестування

Це перша стаття з серії «Введення в культуру DevOps». Попередній матеріал був ввідним, цей присвячений тестуванню. Розглянемо, які стратегії тестування вибрати команді, яка намагається культивувати у себе культуру DevOps.

Тестування і вимоги

Перед тим як заглибитися у вибір стратегії тестування, давайте подумаємо, що таке тестування загалом. А тестування, як таке — це всього лише зіставлення вимог замовника з поточним станом продукту. Також тести можна писати вже існуючий інтерфейс/API. І тоді вони будуть представляти собою зліпок з поточного стану системи, який допоможе при її переробці. Передбачаючи коментарі, скажу, що помилки дуже часто бувають і в самих вимогах, що і їх тестування — зовсім немаловажна деталь процесу. Але ми припустимо, що наш бізнес-аналітик — геній, і він склав ідеальні вимоги, які доставив нам по веселці на рожевому єдинорога.

Не всі тести однаково корисні. Якщо ви тестуєте не поведінка, а реалізацію — тести будуть актуальні дуже короткий час, поки реалізація не буде замінена. І навпаки, якщо тестується поведінку модуля, то тести будуть актуальні до тих пір, поки модуль існує. Не варто писати тести на приватні методи. Це знову повертає нас до того, що ми тестуємо поведінка public-інтерфейсів. Приватні методи — це лише деталі реалізації. З точки зору тестування вони нам не цікаві.

Тести не повинні залежати один від одного. Кожен тест повинен бути атомарен і ні в якому разі не містити залежностей від попередніх. Якщо у вас є такі моменти, то при падінні одного тесту можна відразу поставити хрест на всьому тестовому наборі.

Тепер давайте подумаємо, які бувають вимоги. З точки зору розробника, так і інженера по якості, вимоги поділяються на два великих класи.

Перший клас — це функціональні вимоги. Вони передбачають, як повинна працювати система. Хороший приклад функціональних вимог — опис того, що при натисканні на кнопку «Submit» повинна з'являтися напис «Ok». Це вимога говорить, що повинна робити система . Проте в таких вимогах нічого не сказано про те, як, скажімо, швидко повинна з'явитися напис. Тому з'явився другий клас — нефункціональні вимоги. Вони кажуть, як це повинна робити система. Якщо приходить вимога, що запис має з'являтися не менше ніж за 1 секунду — це типове нефункціональне вимогу. До функціональних відноситься все, що стосується юзабіліті, продуктивності, дизайну і т. д.

Тепер перейдемо до видів тестування. І тут я змушений вставити коментар від капітана очевидності, що тестування буває функціональне і нефункціональне. Також тестування поділяється на ручне й автоматичне. Відразу відкинемо ручне тестування. Воно не вписується в одну з головних практик DevOps «автоматизируй все, що можна». Отже, ми будемо звертати увагу на автоматичні тести.

Типи автотестів

Коли говорять про автотестах, відразу згадуються піраміди. Піраміда тестування допомагає створити нам тестовий план. Однак вона іноді вводить людей в оману, коли справа стосується покриття коду різними видами тестів. Але про все по порядку.

Ми будемо порівнювати автотесты за такими параметрами:

Отже, автотесты діляться на наступні типи: Unit, Integration, End-To-End.

Unit

Це тести, що перевіряють public-інтерфейси окремо взятого модуля. Вони сповідують концепцію white box testing, при якій людині, який тестує систему, доступний весь її вихідний код. Відразу обмовлюся, white box в деяких випадках перетворюється в повноцінний black box з-за того, що іноді модулі розглядаються саме як чорні ящики з вхідними та вихідними даними.

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

Тепер про час розробки. Як було сказано вище, для тестування потрібен окремо взятий модуль або функція. Ця функція повинна бути ізольована від зовнішніх бібліотек і сторонніх модулів. Модуль повинен бути, по можливості, ізольований від системних таймерів та інших взаємодій з системними API. Це забирає досить багато часу на розробку, а, як наслідок, юніт-тести стають одними з найбільш трудозатратных при етапі розробки.

Якщо говорити про витрати на підтримку, юніт-тести — найбільш стабільні при проходженні. Середа їх виконання синтетична і не залежить від зовнішніх факторів. Тобто якщо юніт-тести впали, то на 99% це помилка програми, а не якесь безглузде збіг обставин, коли щось пішло не так, як хотілося.

Проте тут криється момент, який затьмарює рожеву картину загального юніт-тестування. Іноді трапляється, що всі тести проходять, а додаток не працює. Чому? Не варто забувати, що всі модулі і функції працюють в ідеальній синтетичної середовищі, для якої у нас оточення підмінено. Вони лише показують працездатність її окремих частин.

Якщо підсумувати, юніт-тести — ідеальний інструмент перевірки якості коду, але не додатка. Вони показують розробникам, де і що саме вони поламали в режимі реального часу. Вони повинні запускатися на машині розробника. Також вони додаються в прекоммит хуки або CI pipeline, щоб разраб не коммитали завідомо неробочий код.

Integration

Це тести, що перевіряють функціональність взаємодії декількох модулів одночасно. Вони використовують grey-box тестування, при якому ми можемо ставитися до досліджуваного об'єкта, як до чорного ящика і в теж час смикати якісь внутрішні методи, лазити в базу і т. д.

Поклавши руку на серце, практично всі тести, які створюються розробниками, є интегрейшенами. Рідко коли розроблення дизайн упарывается і повністю ізолює систему під тестуванням від абсолютно всіх взаємодій. І якщо API впливають — Integration тести вже залежні від оточення. Вони вже показують те, як ви вмієте деплоить систему.

Розробка цього виду тестів вже не займає стільки часу, скільки потрібно для Unit'ів. Однак ці тести більш крихкі, тому що велика кількість факторів впливає на їх роботу. Їх проходження займає більше часу, ніж у юніт-тестів за рахунок взаємодії з третьесторонними API. Тепер вже розробник не може запускати ці тести після кожного збереження, тому як такий тест триває декілька секунд.

Integration — ідеальний спосіб оцінювати те, в якому стані зараз у вас знаходиться білд на CI. Вони не вимагають багато часу і дозволяють контролювати те, як працюють модулі вашої системи, де і які проблеми у вас вилізуть. Integration повинні запускатися після кожного пуша в репозиторій, щоб розробник міг бачити ті непоправні поліпшення в коді, завдані її діяльністю.

End-To-End

Це тести, що перевіряють всю функціональність в цілому. Такий вид тестів використовує концепцію чорного ящика, при якій додаток являє собою невідому річ, з якою ми співпрацюємо допомогою публічних інтерфейсів. Чомусь завжди Е2Е-тести асоціюються у більшості розробників з Selenium, хоча тестування REST API засобами http-запитів — також типовий зразок Е2Е-тестів, які повністю вкладаються в концепт чорного ящика.

Е2Е-тести перевіряють працездатність системи в цілому. Прогін займає досить багато часу, тому їх не ганяють локально. Я б навіть не рекомендував запускати їх після кожного пуша в центральний репозиторій. Тести ідеальні для запуску після злиття гілок або щодо оточень sandbox або staging — для того, щоб контролювати стан додатка в гілках і на оточеннях. Також при використанні микросервисной архітектури тести можуть покривати відразу кілька микросервисов, що призведе до кращого покриття коду тестами.

E2E-тести залежать від багатьох чинників і надзвичайно тендітні. Іноді лист про заваленому билде може прийти тільки із-за того, що затупил браузер або ж була проблема зі зв'язком на виртуалке (до речі, це твердження справедливо і для Integration).

E2E гарні для перевірки того, що всі заїхало добре. Тобто якщо стоїть завдання перевірити систему на працездатність, E2E — це те, що для вас потрібно. Однак пам'ятайте, що досить часто траплятимуться помилкові тривоги.

Покриття коду тестами

Якщо у вас один тест, який покриває одну не використовується ніде функцію, то користі від нього — нуль. Якщо ж тести покривають всі частини програми, то ви можете бути впевнені, що ймовірність раптових сюрпризів мінімальна.

Тепер давайте обговоримо відсотки покриття тестів. Чомусь покриття прийнято вважати по лініях коду. Тобто у нас є 100 ліній коду, тест (не важливо якого типу) проходиться по 60-ти з них — в результаті отримуємо покриття 60%. Але чи означає це, що у нас все добре? Немає. Так як покритими можуть бути саме ті рядки, в яких ви визначаєте змінні, а код, що містить логіку, може бути просто пропущено. Спеціально для цього були придумані інші метрики покриття коду тестами.

Існують такі метрики покриття коду тестами:

Покриття коду тестами показує рівень того, наскільки ваші тести гарантують працездатність системи при їх успішному проходженні. Це не гарантія того, що все буде добре, але з великою часткою ймовірності все буде в шоколаді.

DevOps і стратегія тестування

DevOps-культура заохочує часті релізи. Часті релізи — це страховка від поломки вашого додатка після непоправних поліпшень розробниками, так як за раз ви будете вивозити менше змін. Однак часті релізи вимагають більше QA-роботи. Тобто якщо ви будете виливатися мало не кожну годину, то ваші інженери з якості просто перестануть ходити додому, їсти, пити і вести соціальне життя.

DevOps-культура пропонує звести до мінімуму ручну QA-роботу, щоб мати можливість релизиться як можна частіше, що безпечніше і стабільніше. У DevOps-культурі роль QA-інженерів зміщується від тестерів до людей, які стежать за якістю проекту та допомагають розробникам в написанні автоматичних тестів, виробляють стратегію тестування. Тут вже мало місця для QA, які не розбираються в технологічних аспектах додатка, CI/CD процесах.

Стратегія тестування — це план, який дозволить вам працювати з мінімальною затратою часу, а отже, і грошей. Стратегія тестування для DevOps не повинна звучати: «Ми використовуємо Protractor і Jenkins». Це не план. Це всього лише перелік інструментів, які використовуються вами. Головне питання, на які повинен відповідати план, — чому й навіщо.

Ми повинні відповісти собі на наступні питання:

Перше, з чого ми повинні почати, — це впровадити концепцію TDD. Відразу скажу, що TDD не є абсолютно правильним вибором, але воно сприяє написання тестів як таких. Тобто розробники повинні писати тести ще до початку кодування. У розробників будь-яка дія збереження повинно триггерить запуск юніт-тестів. Крім того, інженер не повинен мати можливості залити свій код з поламаними тестами або коли тестове покриття падає нижче якоїсь межі.

При пуше коду в фіча бранч повинні прогоняться сьюти integration - та unit-тестів. Вони визначають статус білду і можливість смерджить його в головну dev-гілку. При поламаному билде не повинно бути можливості мерджа, а коммитер повинен бути сповіщений, що його комміт зламав гілку. Далі йде мердж в master або stage brunch і збірка там. При цьому потрібно проганяти всі тестові набори, включаючи E2E.

Цей тестовий набір визначає, чи може здійснюватися поставка вашого продукту. У разі фейла всі члени dev-команди повинні бути сповіщені про те, що поставка відкладена, і про причини, що викликала цю помилку.

Якщо всі набори тестових оточеннях пройшли нормально, здійснюється поставка продукту на продакшен і знову прогін E2E тестових наборів вже на продакшен оточенні. На додаток до перерахованих вище тестових наборів на продакшені повинен бути здійснений прогін performance, penetration та інших функціональних тестів. Якщо щось пішло не так — здійснюється відкат і оповіщення всіх задіяних у процесі деплоймента і розробки. Причому зауважте, що відкат повинен бути протестований E2E та іншими тестами.

Як бачимо, кожен раз ми проганяємо все більш і більш ресурсомісткі тести, відсікаючи при цьому помилки, які можуть бути пов'язані з різними чинниками. У вас утворюється тестовий пайплайн, при якому ваш продукт переходить від одних тестів до інших.

Висновки

Автоматизоване тестування — одна з невід'ємних практик DevOps-культури. Вона дозволяє контролювати вихідний продукт і оперативно усувати помилки, починаючи від процесу розробки і закінчуючи процесом деплоя на продакшн-оточення.

Також хочу сказати велике спасибі всім, хто залишає коментарі на мої статті, всім людям, які беруть активну участь в обговоренні цієї серії статей.

Опубліковано: 03/08/18 @ 10:12
Розділ Різне

Рекомендуємо:

Кар'єрні рішення на прикладі комп'ютерних ігор початку 2000-х
DOU Проектор: tabXpert – Chrome-розширення для ефективного управління вкладками
Переваги й недоліки релокації у Чехію – розповідь українця з Amazon
Android дайджест #31: Android Studio, Google Play, ML Kit
Реалізація JNI callbacks в Android NDK