Тестування мікросервісної архітектури (MSA/Microservices)
Підхід до тестування мікросервісної архітектури відрізняється від звичного. Вона являє собою сукупність дрібних сервісів, кожен з яких відповідає за певний функціонал, а разом вони є готовим додатком і вирішують певне глобальне завдання.
Головною перевагою та одночасно труднощами тестування є те, що вони розташовуються на різних серверах і написані різними мовами програмування, таких як Java та .Net. Практично розробники певного мікросервісу не знають, що роблять інші мікросервіси, що ускладнює процес тестування. Але ми можемо швидко оновити і протестувати окремий мікросервіс, не торкнувшись інші.
Саме тестування можна розділити на такі види:
unit тестування;
контрактне тестування;
інтеграційне тестування;
end-to-end тестування;
тестування навантаження;
UI чи функціональне тестування.
Unit тестування
Ідея в тому, щоб писати тести для кожної нетривіальної функції чи методу. Це дозволяє досить швидко перевірити, чи не призвела чергова зміна коду до регресії, тобто появи помилок у вже протестованих місцях програми, а також полегшує виявлення та усунення таких помилок. Unit тестування буває позитивне, тобто спрямоване на перевірку поведінки методів у нормальних умовах, та негативне, яке покликане перевірити стійкість системи до позаштатних ситуацій.
У нас процеси були збудовані так, що девелопери на етапі розробки функціоналу самостійно пишуть Unit-тести. Кожен із них сам краще знає, як його код працює, і може ефективніше впоратися із цим завданням, ніж тестувальники.
Unit тестами у нас покрито 70% функціоналу, і оскільки ми застосовуємо CI/CD, доки вони не пройдені, програма не задеплоїться.
Контрактне тестування
Як я вже згадав, над мікросервісами завжди працює кілька команд: бекенд, фронтенд та тестувальники. Всі вони повинні між собою домовитися: який ендпоінт працює з якими параметрами, і що прийматиме і повертатиме кожен тип даних.
Для цього потрібен контракт між командами (у нашому випадку ми використовуємо Pact), який міститиме всі методи та повернення для всіх сервісів.
Наприклад, бекенд-розробник написав код, простовив анотації та зробив swagger-документацію. Але якщо swagger не провалідується фронтендом, а QA його вже протестують – ми просто дарма витратимо час. Тому і створюється контракт: наприклад, сервіс має 8 ендпоінтів, і ми знаємо, в якому форматі він віддає і приймає дані.
Контрактне тестування необхідне для того, щоб переконатися, що все так і працює. По суті, це тестування чорної шухляди: як відбуваються процеси всередині сервісу неважливо, якщо якась схема працює не по пакту - це баг.
У нас воно працює таким чином: з ранньої стадії є технічне завдання, узгоджене з усіма стейкхолдерами. На основі ТЗ проходить оцінка завдання та створюється схема, згідно з якою всі працюватимуть.
Так QA може писати первинні сценарії перевірки бекенда, який ще навіть не існує. Контракт допомагає зрозуміти, що очікувати. Звичайно, на практиці не все працює гладко, не по всіх мікросервісах вдалося створити контракт до початку розробки. Фронтенд-розробників у нас менше, ніж бекенд, і їм доводиться підтягуватись вже після розробки.
Інтеграційне тестування
Це один із найкритичніших тестів усієї архітектури. При позитивному результаті тестування ми можемо бути впевнені, що вона спроектована правильно, і всі незалежні мікросервіси функціонують як єдине ціле відповідно до очікувань.
Так як процес тестування був впроваджений на ранніх етапах розробки, кожен окремий сервіс довелося піднімати локально, а залежність інших модулів - мокати. Як мову було обрано Java (з моменту заснування компанії ця мова використовується для написання тестів).
Що стосується збирача, то все також дуже просто: наша поточна архітектура спочатку використовує Gradle.
Якщо розглянути нашу інфраструктуру автоматизації, то це переважно кастомний проект, в основі якого Java і Gradle, плюс купа бібліотек, таких як Junit5, Feign, Rest Assured і т.д.
Feign та Rest Assured використовується разом тому, що до переходу на мікросервісну архітектуру наш проект чудово жив на Feign. Як бібліотека для покриття API це було найкраще рішення. Але після переходу на нову архітектуру за фактом всю платформу було переписано на мікросервіси, які потрібно покрити верхньорівневими тестами в короткий термін для подальшого проведення інтеграційного тестування. Тут ми і підключили другу бібліотеку Rest Assured, що дозволило швидко покривати величезні шматки функціоналу (для багатьох це рішення буде спірним, але на той момент більшість нових QA працювала саме з Rest Assured, що стало вирішальним фактором при виборі нової бібліотеки).
Для розгортання оточення в docker-контейнерах локально або на CI-сервері використовується Java-бібліотека TestContainers, яка дозволяє оркеструвати docker-контейнерами безпосередньо з тестового коду (testcontainers.org). При розгортанні оточення піднімаються сам сервіс, що тестується, а також використовувані сервісом бази даних, брокер повідомлень і емулятор, який і мокає всі зовнішні сервіси.
Так як всі контейнери ми піднімаємо локально і на ранніх етапах розробки, потрібно було дуже багато часу на те, щоб налаштувати перманентне оточення. Наприклад, у нас є сервіс Settings, якому для роботи потрібні Сервіси 1 та 2, та якісь дані з Kafka та MINO. Усе це береться зі змінних оточення, і з допомогою великої кількості залежностей важко контролювати процес підняття одного сервісу.
Тестування формально поділяється на автоматизоване та ручне. Мануальні тестувальники проводять тести руками, не піднімаючи середовища і пишуть тест-кейси для автоматизаторів, спрощуючи їм завдання. У нас два мануальники покривають п'ять автоматизаторів - дуже зручно.
End-to-end тестування
По суті, E2E тестує бізнес-логіку, так само, як і в інтеграційному, але вже не ізольовано, а в масштабі всієї системи.
В end-to-end тестуванні ми перевіряємо взаємодію всіх сервісів з платформою:
реєстрація, авторизація, ігрова діяльність, поповнення та зняття коштів, іншими словами, перевіряємо здатність всього додатка задовольнити всі запити кінцевого користувача. Так само, як при інтеграційному тестуванні - мікросервіси піднімаються локально, але дані вже не мокаються. Сервіси спілкуються один з одним. За фактом на локальній машині здіймається кінцевий продукт. Сценарій є максимально наближеним до запуску.
Тестування навантаження
Процес навантажувального тестування будемо формально розділяти на 4 невеликі етапи:
тестування продуктивності (Performance Testing) - дослідження часу відгуку ПЗ під час операцій на різних навантаженнях, зокрема на стресових навантаженнях;
тестування стабільності чи надійності - дослідження стійкості ПЗ в режимі тривалого використання з метою виявлення витоків пам'яті, перезапуску серверів та інших аспектів, що впливають на навантаження;
стрес-тестування - дослідження працездатності ПЗ в умовах стресу, коли навантаження перевищує максимально допустимі значення, для перевірки здатності системи до регенерації після стресового стану, а також для аналізу поведінки системи при зміні аварійної апаратної конфігурації;
об'ємне тестування (Volume Testing) - дослідження продуктивності програмного забезпечення для прогнозування довгострокового використання системи при збільшенні обсягів даних, тобто аналіз готовності системи до довгострокового використання.
Для тестів ми використовуємо JMeter, а самі скрипти навантаження написані на Groovy.
Ми використовуємо близько п'яти віртуальних машин, розгорнутих на AWS, і у нас є 7 фізичних машин. Останні використовуємо, якщо потрібно створити велике навантаження – 15,000 RPS і більше. Віртуальні машини, оскільки вони, по суті, є «відкусаними» частинами однієї великої машини, таких цифр показати не можуть – кожен реквест потрібно відправляти з підписом шифрування, і це навантажує процесор. Так що VM використовуємо для фонового або статичного навантаження близько 2000 RPS.
Статистику збираємо в Grafana – аналізуємо всі показники, навантаження на CPU, GPU, мережу, диски тощо.
Спочатку порівнюємо з еталонними показниками, потім експериментуємо, наприклад, навантажуємо якийсь час процесор на 30%, робимо короткий стрибок до 90%-100%, і дивимося, скільки битих запитів нам нападає.
UI або функціональне тестування
Це завершальний етап тестування. Якщо попередніх тестах фігурував лише API, тепер тестується і фронт. Проводимо як мануальне тестування, і автотести.
Першочергове завдання – це мінімальне функціональне тестування. Тестуються готові збирання, які вже можна показати замовникам. Поки воно не пройдено, сенсу далі тестувати нема.
Ми використовуємо Java, Cucumber та самописну бібліотеку для опису логіки сценаріїв (раніше використовували Akit сценарії, але підтримка бібліотеки закінчилася на Java 8, нам довелося написати свою бібліотеку для роботи з UI-тестами, але в основі лежать методи саме звідти). Cucumber використовуємо для зручності написання самих тестів.
Оскільки проект великий, нам необхідно запускати величезну кількість скриптів одночасно, для вирішення цієї проблеми ми використовуємо Selenide, який розгорнуть на одному оточенні з Jenkins.
У Jenkins створюється pipeline, в якому прописуємо скільки контейнерів необхідно підняти для запуску тесту.
Наприклад, потрібно протестувати email-шаблони, яких ми маємо 100-200. В один потік це триватиме 15-20 хвилин. Тому створюємо pipeline в Jenkins, який цей скрипт розбиває на багато маленьких контейнерів, які піднімаються Selenide.
Можна сказати, що Selenide – це віртуальний браузер, а Selenium – віртуальний користувач. Одночасно піднімаються 10 контейнерів і всі тести проходять за пару хвилин. Усі пайплайни теж написані на Groovy.
Після цього збираємо це все до звітів залежно від проекту: UI у Cucumber reports, а API-тести – у Azure.
Усі скрипти пишуться на основі тест-кейсів та юз-кейсів, які роблять мануальні тестувальники. Перед початком розробки мануальники мають ТЗ, в якому описано бізнес-логіку програми, і макети від дизайнерів.
Джерела:
Дод. матеріал:
Last updated