Складнощі тестування мікросервісів та що з ними робити
У червні 2019 року я виступив на конференції ProQA.Today на тему тестування мікросервісів. Якщо коротко, то в моїй доповіді було чимало критики й могло скластися враження, що я затятий противник технології, але це не так — удома я навіть маю свій Docker Registry на окремому сервері, з багатьма контейнерами для різних тестерських експериментів. А в Google Cloud у мене є власний застосунок. Як і в будь-якій технології, я бачу в мікросервісах сильні і слабкі сторони, де чимало залежить від правильної архітектури й способу використання. Кілька місяців я обмірковував свою доповідь, виступи інших спікерів та критику й готовий структурувати свої думки у статтю. Усі приклади нижче — мій досвід тестування монолітних систем і мікросервісів.
Точка відліку
Для початку визначмо терміни, щоб розуміти, про що ми говоримо. У моєму розумінні моноліт — ПЗ як одна система, яке можна розгорнути й запустити на одному сервері. Усі модулі, що відповідають за різну бізнес-логіку, вміщено в одній програмі й запущено в одному процесі.
Мікросервіси надають змогу виконувати ту ж саму роботу, що й моноліт, але розподіливши модулі в окремі віртуальні машини-контейнери за принципом «один модуль — одне завдання — один контейнер» і зв’язавши їх за допомогою віртуальної мережі.
Що мені не подобається в тестуванні мікросервісів
Мікросервіси складно тестувати атомарно. У 99% випадків не можна запустити один окремий мікросервіс, щоб протестувати його REST веб-сервіси, наприклад, не створивши спочатку мок усіх пов’язаних з ним сервісів. Якщо в команди розробки немає часу робити моки, можна забути про інтеграційне тестування на умовному Jenkins’і у вакуумі. Уже тут мене запитують: а як же тестування контрактів? Як я розумію, тестування контрактів лише перевіряє відповідність веб-сервісів специфікації (swagger чи щось подібне), а мене цікавить саме тестування веб-сервісів з визначеними сетами даних. З монолітами зазвичай простіше — хоч локально розгорни собі систему й тестуй.
Для мікросервісів важко керувати даними. Кожен мікросервіс може мати свою окрему базу даних, ніяк не пов’язану з базами інших мікросервісів. З погляду автономності це, звісно, правильно, але є одне «але»: якщо сутності (об’єкти) в системі, що ми тестуємо, складаються з менших сутностей кожного мікросервісу (як Вольтрон), база даних не може забезпечити їхньої цілісності. Для зв’язності даних у кожному мікросервісі потрібно створювати окремий механізм, наприклад, зберігати ID сутностей інших сервісів. Проблема в тому, що дані одного мікросервісу можна стерти й помилку можна знайти лише на етапі тестування (або в продакшені). А під час створення/заміни даних потрібно запам’ятовувати ID одних об’єктів, щоб потім зберегти їх у базах інших мікросервісів. Єдиним робочим підходом нашої команди стало створення й редагування даних через UI в невеликих обсягах під кожний конкретний тест — так довше, ніж прямо через БД, але простіше.
Для мікросервісів важко забезпечити транзакційність. Тут як у поганому анекдоті: «Студент, щоб скласти сесію, дав хабаря викладачеві, потім секретарю декана, а потім декан хабаря не взяв. Жаль, що хабарі не транзакційні й перші двоє грошей не повернуть». Тестування транзакцій у мікросервісах подібне до цього жарту: перший сервіс робить зміни в даних і передає їх другому сервісу — другий сервіс робить зміни у своїх даних і передає третьому, і тут трапляється помилка! Не просто помилка, а наприклад, необроблений Null Pointer Exception. Тест не пройшов, дані перших двох мікросервісів не відповідають іншим — потрібно вручну їх чистити. Ба більше, для забезпечення транзакційності між різними мікросервісами розробникам треба писати більше коду, більше веб-сервісів для зворотного зв’язку (що потребує більше часу на тестування). А найгірше те, що з погляду цілої системи може бути зовсім не очевидно, в якому саме мікросервісі сталася помилка — потрібно брати логи й дані всіх залучених сервісів і перевіряти. У монолітах транзакційність часто гарантують на рівні БД.
Мікросервіси можуть використовувати різні канали зв’язку. Коли розпочинають розробку нової системи, кожен розробник мріє про простий уніфікований інтерфейс. Наразі дуже популярний, наприклад, REST. «Нехай усі наші мікросервіси взаємодіятимуть тільки через REST, казали вони...» Проте під час розробки щоразу трапляються ситуації:
- цей 3rd-party-мікросервіс взаємодіє лише через SOAP, ми не можемо нічого вдіяти, під’єднуватимемо, як є;
- для реалізації цієї фічі потрібно передавати інформацію декільком мікросервісам одночасно й асинхронно — скористаймося JMS, це просто й надійно;
- у нас тут є legacy-система, вона працює лише через свій власний протокол на базі TCP;
- нам потрібно гарантувати синхронізацію «дуже важливих даних»™, тому нехай перший сервіс писатиме дані в окрему базу, а інший сервіс раз на хвилину їх вичитуватиме.
Кожний новий канал комунікації ускладнює тестування й часто потребує встановлення спеціальних програм для роботи.
Для системи мікросервісів важко зробити автоматизоване UI-тестування. Насправді розробити самі автотести легко, і той же Selenium WebDriver добре виконує свою роботу. Проблеми з’являються, коли ми намагаємося додати автотести в CI/CD. З монолітом усе просто:
- Новий білд.
- Автотести.
- ???
- Profit.
Складнощі починаються, коли система складається з 2+ мікросервісів:
Q: Де виконувати регресійні тести?
A: На умовному Jenkins потрібно створити велику кількість моків.
Q: Може, після деплою?
А: А якщо під час виконання тесту почнеться деплой іншого мікросервісу?
Q: Ставити всі білди в чергу?
А: А якщо черга стане завелика?
Кожне запитання породжує низку інших запитань. У результаті найдієвішим компромісом для нашої команди обрано щонічне виконання тестів в окремому середовищі лише для автотестів (щоб контролювати дані) і мануальне регулювання деплою в це окреме середовище.
До того ж я виявив, що значно ефективніше писати невеликі набори тестів і запускати їх локально, на допомогу мануальному тестуванню.
Мікросервіси потребують більше ендпоїнтів і даних між сервісами. Моноліт може містити чимало веб-сервісів, створених для виконання бізнес-функцій програми. Але в мікросервісів їх буде більше, оскільки, крім тих самих сервісів для виконання бізнес-функцій, ми додаємо ще технічні, для зв’язку мікросервісів між собою. І кожен потребує тестування.
Кожен веб-сервіс одночасно повинен передавати більше даних, бо буває, що перший мікросервіс передає дані другому, той використовує два умовні поля для своїх обрахунків (наприклад, лише перевіряє ID), а всі інші дані передає наступному сервісу, якому вони корисні (на рисунку схема передачі корисного навантаження payload між мікросервісами. Поля, що використовує сервіс — підсвічено).
Більше даних — більша ймовірність бага — більше тестів. Тому немає потреби перевіряти всі можливі комбінації параметрів — тут варто зосередити тестування лише на найризикованіших і найпріоритетніших сценаріях.
У мікросервісах важче знайти першопричину помилки. Базова інструкція тестувальника вимагає, що в разі знаходження бага потрібно його відтворити й зарепортити з усіма передумовами, кроками, сподіваними результатами тощо. Однак кожний тестувальник знає, що в разі складних систем для успішного фіксу, краще знайти першопричину бага, додати логи й дані, щоб не отримати cannot reproduce. І в мікросервісах це може стати справжньою проблемою, якщо одна дія користувача спричинює ланцюгову реакцію, де залучено чимало систем. У якому сервісі помилка?
Помилка виникла через баг у коді мікросервісу чи через те, що інший сервіс передав йому неправильні дані? А може, помилки в коді взагалі немає, просто операція завершилася за timeout, і проблема в інфраструктурі? Ще гірше, якщо баг виникає в проді, до бази й логів якого команда не має доступу через GDPR. Поради, що робити, немає — тут рятує лише відмінне знання командою системи й можливість припустити причину з великою ймовірністю.
Мікросервіси потребують чимало ресурсів. Я пам’ятаю ті часи, коли мав низку невеликих тестових серверів на Tomcat з кількома гігабайтами RAM, у які міг сам задеплоїти нову версію продукту й почати тестування. Навіть робити редеплой попередніх версій, щоб визначити, коли саме з’явилася помилка. Нині середовище тестування системи мікросервісів, в якому я працюю, потребує 64 ГБ. А іноді й цього замало! Я не сказав би, що інфраструктура — це безпосередня проблема тест-ліда, але коли в системі ліміт ресурсів, то це може спричинити баги й неможливість вчасно додати ще один контейнер з тестами. Кожен контейнер мікросервісу — власна віртуальна машина, що використовує ресурси, у разі ж моноліту (який теж можна запакувати в докер за потреби) — у нас є лише один сервер чи віртуальна машина.
Висновки
Тепер спробую позитивно підсумувати все вище написане :) Я не закликаю відмовлятися від мікросервісів — лише бути готовим до складнощів, які ця технологія може спричинити, зокрема:
- Вимагати якомога більше прав для роботи із середовищем — консольні утиліти для отримання логів і під’єднання до контейнера можуть значно спростити життя.
- Вимагати зберігати всю критичну інформацію контейнерів (логи, файли) в постійному сховищі (база даних, FTP, спільна тека), бо в разі крешу контейнера чи деплою нової версії мікросервісу, дані можуть бути втрачені.
- Ознайомитися з докером (що це, як працює) і командами оркестраторів типу Kubernetes, Openshift (вони майже ідентичні).
- Не створювати багато тестових даних заздалегідь — вони швидко втрачають актуальність.
- Постійно вивчати розроблену систему не лише за вимогами, а й з погляду архітектури — так буде легше розуміти, що й де може піти не так.
- Обов’язково тестувати навантаження, щоб виявити вузькі місця й баги синхронізації.
- Вивчити хоча б одну мову програмування — невеликі скрипти можуть значно полегшити вашу роботу.
Дякую, що дочитали. Напишіть про ваш досвід, мені справді цікаво знати success story :)
І наостанок — ми з колегами створили телеграм-канал і намагаємося регулярно публікувати там щось цікаве про тестування: історії з власного досвіду, інформацію про знайдені корисні тули, різні гайди та лайфхаки. Підписуйтеся, якщо цікаво.