Автоматичне тестування мікросервісів у Docker для безперервної інтеграції

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

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

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

Автоматизація такого підходу становить низку проблем, вирішення яких буде описано трохи нижче:

  • конфлікти паралельних завдань в одному докер-хості;
  • конфлікти ідентифікаторів БД при ітераціях тесту;
  • очікування на готовність мікросервісів;
  • об'єднання та виведення логів у зовнішні системи;
  • тестування вихідних HTTP-запитів;
  • тестування веб-сокетів (за допомогою SignalR);
  • тестування аутентифікації та авторизації OAuth.

Це стаття з мотивів мого виступу на SECR 2019. Так що для тих, кому ліньки читати, ось запис виступу.

Автоматичне тестування мікросервісів у Docker для безперервної інтеграції

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

Один і той же скрипт запускають як самі розробники на Windows-десктопах, так і сервер Gitlab CI під Linux.

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

Тест повинен працювати на локальному сервері з наступних причин:

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

Крім того, задіяти стенд небажано, тому що:

  • Зламати стенд може не тільки поганий код, який працює на ньому, але й дані, які правильний код не може обробити;
  • Як би ми не намагалися повертати назад усі зміни, зроблені тестом, у ході самого тесту щось може піти не так (інакше, навіщо тест?).

Про проект та організацію процесу

Наша компанія розробляла мікросервісний веб-додаток, що працює в Docker у хмарі Amazon AWS. На проекті вже використовувалися юніт-тести, проте часто виникали помилки, які юніт-тести не виявляли. Потрібно тестувати цілий мікросервіс разом із базою даних та сервісами Amazon.

На проекті використовується стандартний процес безперервної інтеграції, що включає тестування мікросервісу при кожному коміті. Після призначення завдання розробник вносить зміни до мікросервісу, сам його тестує вручну і запускає всі наявні автоматичні тести. За потреби розробник змінює тести. Якщо проблеми не виявлено, робиться коміт у гілку цього завдання. Після кожного комміту на сервері автоматично запускаються тести. Мерж у загальну гілку та запуск автоматичних тестів на ній відбувається після успішного реву. Якщо тести на гілці пройшли, сервіс автоматично оновлюється в тестовому оточенні на Amazon Elastic Container Service (стенді). Стенд необхідний усім розробникам та тестувальникам, і ламати його небажано. Тестувальники на цьому оточенні перевіряють фікс або нову фічу, виконуючи ручні тести.

Архітектура проекту

Автоматичне тестування мікросервісів у Docker для безперервної інтеграції

Додаток складається з більш як десяти сервісів. Деякі з них написані на .NET Core, а деякі на NodeJs. Кожен сервіс працює в Docker-контейнері в Amazon Elastic Container Service. Кожен має свою базу Postgres, а в деяких ще й Redis. Спільних баз немає. Якщо кільком сервісам потрібні одні й самі дані, ці дані у момент зміни передаються кожному з цих сервісів через SNS (Simple Notification Service) і SQS (Amazon Simple Queue Service), і сервіси зберігають в свої відокремлені бази.

SQS та SNS

SQS дозволяє за протоколом HTTPS класти повідомлення в чергу та читати повідомлення з черги.

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

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

У SNS ви створюєте topic та підписуєте на нього, наприклад, SQS-чергу. У topic можна надсилати повідомлення. При цьому повідомлення надсилається в кожну чергу, підписану на цей topic. У SNS немає методу для читання повідомлень. Якщо в процесі налагодження або тестування потрібно дізнатися, що відправляється в SNS, можна створити SQS-чергу, підписати її на потрібний topic і читати чергу.

Автоматичне тестування мікросервісів у Docker для безперервної інтеграції

Шлюз API

Більшість сервісів недоступні безпосередньо з інтернету. Доступ здійснюється через API Gateway, який перевіряє права доступу. Це також наш сервіс, і для нього теж є тести.

Повідомлення у реальному часі

Додаток використовує СигналR, щоб показувати користувачеві повідомлення в реальному часі. Це реалізовано у сервісі повідомлень. Він доступний безпосередньо з інтернету і сам працює з OAuth, тому що вбудувати підтримку Web-сокетів у Gateway виявилося недоцільним, порівняно з інтеграцією OAuth та сервісу повідомлень.

Відомий підхід до тестування

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

В статті від Microsoft пропонується використовувати in-memory базу та впроваджувати мок-об'єкти.

In-memory база – ця одна із СУБД, які підтримує Entity Framework. Вона створена спеціально для тестів. Дані в такій базі зберігаються лише до завершення процесу, що її використовує. У ній не потрібно створювати таблиці і цілісність даних не перевіряється.

Мок-об'єкти моделюють клас, що заміщується, лише настільки, наскільки розробник тесту розуміє його роботу.

Як домогтися автоматичного запуску Postgres та виконання міграції під час запуску тесту, у статті Microsoft не вказано. Моє рішення робить це і, крім того, в сам мікровервіс не додається ніякий код спеціально для тестів.

Переходимо до рішення

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

Налаштування тестового оточення

Перше завдання – розгорнути тестове оточення. Кроки, які необхідні для запуску мікросервісу:

  • Налаштувати тестований сервіс на локальне оточення, у змінних оточення вказуються реквізити для підключення до бази та AWS;
  • Запустити Postgres та виконати міграцію, запустивши Liquibase.
    У реляційних СУБД перед тим, як записувати дані до бази, необхідно створити схему даних, простіше кажучи, таблиці. При оновленні програми таблиці потрібно привести до вигляду, який використовується новою версією, причому, бажано, без втрати даних. Це називається міграція. Створення таблиць спочатку порожній базі – окремий випадок міграції. Міграцію можна вбудувати в саму програму. І в .NET, і в NodeJS є фреймворки для міграції. У нашому випадку з метою безпеки мікросервіси позбавлені права змінювати схему даних, і міграція виконується за допомогою Liquibase.
  • Запустіть Amazon LocalStack. Це реалізація сервісів AWS для запуску. Для LocalStack є готовий образ у Docker Hub.
  • Запустити скрипт для створення в LocalStack необхідної сутності. Shell-скрипти використовують AWS CLI.

Для тестування на проекті використовується Листоноша. Він був і раніше, але його запускали вручну і тестували програму, вже розгорнуту на стенді. Цей інструмент дозволяє робити довільні запити HTTP(S) і перевіряти відповідність відповідей очікуванням. Запити поєднуються в колекцію, і можна запустити всю колекцію повністю.

Автоматичне тестування мікросервісів у Docker для безперервної інтеграції

Як влаштований автоматичний тест

Під час тесту в Docker працює все: і сервіс, що тестується, і Postgres, і інструмент для міграції, і Postman, а, вірніше, його консольна версія - Newman.

Docker вирішує цілу низку проблем:

  • Незалежність від конфігурації хоста;
  • Установка залежностей: докер завантажує образи з Docker Hub;
  • Повернення системи до вихідного стану: просто видаляємо контейнери.

Докер-композ об'єднує контейнери у віртуальну мережу, ізольовану від інтернету, де контейнери знаходять один одного за доменними іменами.

Тестом керує shell-скрипт. Для запуску тесту під Windows використовуємо git-bash. Таким чином, достатньо одного скрипту і для Windows, і для Linux. Git та Docker встановлені у всіх розробників на проекті. При установці Git під Windows встановлюється git-bash, тому він теж у всіх є.

Скрипт виконує такі кроки:

  • Побудова докер-образів
    docker-compose build
  • Запуск БД та LocalStack
    docker-compose up -d <контейнер>
  • Міграція БД та підготовка LocalStack
    docker-compose run <контейнер>
  • Запуск тестованого сервісу
    docker-compose up -d <сервис>
  • Запуск тесту (Newman)
  • Зупинка всіх контейнерів
    docker-compose down
  • Постінг результатів у Slack
    У нас є чат, куди потрапляють повідомлення із зеленою галочкою або червоним хрестиком та посиланням на балку.

У цих кроках задіяні такі Docker-образи:

  • Тестований сервіс – той самий образ, що й у продакшена. Конфігурація для тесту через змінні оточення.
  • Для Postgres, Redis та LocalStack використовуються готові образи з Docker Hub. Для Liquibase та Newman теж є готові образи. Ми будуємо свої на їх кістяку, додаючи туди наші файли.
  • Для підготовки LocalStack використовується готовий образ AWS CLI і на його основі створюється образ, що містить скрипт.

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

Проблеми, з якими можна зіткнутися

Очікування готовності

Коли контейнер із сервісом запущено, це ще не означає, що він готовий приймати з'єднання. Потрібно дочекатися з'єднання, щоб продовжити.

Це завдання іноді вирішують за допомогою скрипту. wait-for-it.sh, який чекає на можливість встановити TCP-з'єднання. Однак LocalStack може помилитися 502 Bad Gateway. Крім того, він складається з безлічі сервісів, і якщо один з них готовий, це нічого не говорить про інші.

Рішення: скрипти підготовки LocalStack, які чекають на відповідь 200 і від SQS, і від SNS.

Конфлікти паралельних завдань

Декілька тестів можуть працювати одночасно в одному Docker-хості, тому імена контейнерів і мереж повинні бути унікальними. Більше того, тести з різних гілок одного сервісу також можуть працювати одночасно, тому недостатньо прописати у кожному compose-файлі свої імена.

Рішення: скрипт визначає унікальне значення змінної COMPOSE_PROJECT_NAME.

Особливості Windows

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

  1. Шелл-скрипти у контейнері повинні мати лінуксові кінці рядків.
    Символ CR для шелла – це синтаксична помилка. За повідомленням про помилку, складно зрозуміти, що справа в цьому. При редагуванні таких скриптів у Windows потрібний правильний текстовий редактор. Крім того, система контролю версій має бути налаштована належним чином.

Ось так налаштовується git:

git config core.autocrlf input

  1. Git-bash емулює стандартні папки Linux і при викликі exe-файлу (у тому числі docker.exe) замінює абсолютні шляхи Linux на Windows-шляху. Однак це не має сенсу для колій не на локальній машині (або колій у контейнері). Така поведінка не вимикається.

Рішення: дописувати додатковий слеш на початок шляху: //bin замість /bin. Linux розуміє такі шляхи, йому кілька слешей – те, що один. Але git-bash такі шляхи не розпізнає та не намагається перетворювати.

Виведення логів

При виконанні тестів хотілося б бачити логи і від Newman, і від сервісу, що тестується. Так як події цих логів пов'язані між собою, поєднання їх в одній консолі набагато зручніше двох окремих файлів. Newman запускається через docker-compose run, і тому його виведення потрапляє у консоль. Залишається зробити так, щоб туди потрапляв висновок сервісу.

Початкове рішення полягало в тому, щоб робити докер-створити без прапора -d, але, використовуючи можливості шелла, відправляти цей процес у фон:

docker-compose up <service> &

Це працювало доти, доки не потрібно було відправляти логи з докера до стороннього сервісу. докер-створити перестав виводити логи у консоль. Проте працювала команда Докер приєднати.

Рішення:

docker attach --no-stdin ${COMPOSE_PROJECT_NAME}_<сервис>_1 &

Конфлікт ідентифікаторів під час ітерацій тесту

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

Щоб його не було, або ID мають бути унікальними, або треба видаляти всі об'єкти, створені тестом. Деякі об'єкти не можна видаляти відповідно до вимог.

Рішення: генерувати GUID-и скриптами в Postman

var uuid = require('uuid');
var myid = uuid.v4();
pm.environment.set('myUUID', myid);

Потім у запиті використовувати символ {{myUUID}}, який буде замінено значенням змінної.

Взаємодія через LocalStack

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

Рішення: запити з Postman до LocalStack.

API сервісів AWS документовано, що дозволяє виконувати запити без SDK.

Якщо сервіс пише у чергу, ми її читаємо і перевіряємо вміст повідомлення.

Якщо сервіс відправляє повідомлення в SNS, на етапі підготовки LocalStack створюється ще й черга і підписується цей SNS-топік. Далі все зводиться до описаного вище.

Якщо сервіс повинен прочитати повідомлення з черги, то на попередньому етапі тесту ми пишемо це повідомлення в чергу.

Тестування HTTP-запитів, що виходять від мікросервісу, що тестується.

Деякі сервіси працюють з HTTP з чимось, крім AWS, і деякі функції AWS не реалізовані в LocalStack.

Рішення: у цих випадках може допомогти MockServer, у якого є готовий образ Докер-концентратор. Очікувані запити та відповіді на них налаштовуються HTTP-запитом. API документовано, тому робимо запити із Postman.

Тестування аутентифікації та авторизації OAuth

Ми використовуємо OAuth та Веб-токени JSON (JWT). Для тесту потрібен OAuth-провайдер, який зможемо запустити локально.

Вся взаємодія сервісу з OAuth-провайдером зводиться до двох запитів: спочатку запитується конфігурація /.well-known/openid-configuration, а потім запитується публічний ключ (JWKS) за адресою конфігурації. Усе це статичний контент.

Рішення: наш тестовий OAuth-провайдер – це сервер статичного контенту та два файли на ньому. Токен згенерований один раз і закоммічений у Git.

Особливості тестування SignalR

Із веб-сокетами Postman не працює. Для тестування SignalR було створено спеціальний інструмент.

Клієнтом SignalR може бути не лише браузер. Для нього є клієнтська бібліотека під .NET Core. Клієнт, написаний на .NET Core, встановлює з'єднання, проходить автентифікацію та очікує певної послідовності повідомлень. Якщо отримане несподіване повідомлення або з'єднання розривається, клієнт завершується з кодом 1. Після отримання останнього очікуваного повідомлення завершується кодом 0.

Поруч із клієнтом працює Newman. Клієнтів запускається кілька, щоб перевірити, що повідомлення надсилаються всім, кому треба.

Автоматичне тестування мікросервісів у Docker для безперервної інтеграції

Для запуску кількох клієнтів використовується опція -scale у командному рядку docker-compose.

Перед запуском Postman скрипт чекає на встановлення з'єднання всіма клієнтами.
Проблема очікування з'єднання вже зустрічалася. Але там були сервери, а тут – клієнт. Потрібен інший підхід.

Рішення: клієнт у контейнері використовує механізм Перевірка здоров'я, щоб повідомити скрипт на хості про свій статус. Клієнт створює файл по певному шляху, скажімо, /healthcheck, як тільки з'єднання встановлено. HealthCheck-скрипт у докер-файлі виглядає так:

HEALTHCHECK --interval=3s CMD if [ ! -e /healthcheck ]; then false; fi

Команда докер перевірити показує для контейнера звичайний статус, health-статус та код завершення.

Після завершення Newman скрипт перевіряє, що всі контейнери з клієнтом завершилися, причому, з кодом 0.

Щастя є

Після того, як ми подолали описані вище складності, у нас з'явився набір тестів, що стабільно працюють. У тестах кожен сервіс працює як єдине ціле, взаємодіє з базою даних та з Amazon LocalStack.

Ці тести захищають команду з 30+ розробників від помилок у додатку зі складною взаємодією 10+ мікросервісів за частих деплой.

Джерело: habr.com

Додати коментар або відгук