Автоматизирано тестване на микросервизи в Docker за непрекъсната интеграция

В проектите, свързани с разработка на микросервисна архитектура, CI/CD преминава от категорията на приятна възможност в категорията на спешна необходимост. Автоматизираното тестване е неразривна част от непрекъснатата интеграция, подход, който може да осигури на екипа множество приятни вечери с семейство и приятели. В противен случай проектът рискува никога да не бъде завършен.

Възможно е да се покрие целият код на микросервиса с юнит-тестове и мок-обекти, но това само частично решава проблема и оставя множество въпроси и трудности, особено при тестването на работата с данни. Както винаги, най-остри са проблемите с тестването на консистентността на данните в релационна БД, тестването на работа с облачни услуги и неверни предположения при написването на мок-обекти.

Всичко това и още малко се решава чрез тестването на целия микросервис в Docker-контейнер. Неоспоримо предимство за осигуряване на валидността на тестовете е, че тестовете преминават през същите Docker-образи, които отиват в продукция.

Автоматизацията на този подход представлява редица проблеми, решението на които ще бъде описано малко по-долу:

  • конфликти на паралелни задачи в един Docker хост;
  • конфликти на идентификатори в БД при итерации на теста;
  • изчакване на готовността на микросервисите;
  • обединяване и извеждане на логовете във външни системи;
  • тестване на изходящи HTTP заявки;
  • тестване на уебсокети (с помощта на SignalR);
  • тестване на аутентификация и авторизация с OAuth.

Тази статия е по мотиви от моята презентация на SECR 2019. Така че за тези, които не им се чете, ето запис на презентацията.

Автоматизирано тестване на микросервизи в Docker за непрекъсната интеграция

В статията ще разкажа как с помощта на скрипт да стартираме тестваната услуга в Docker, базата данни и услугите на Amazon AWS, след което тестовете на Postman и след приключването им да спрем и изтрием създадените контейнери. Тестовете се изпълняват при всяка промяна в кода. По този начин се уверяваме, че всяка версия коректно работи с базата данни и услугите на AWS.

Същият скрипт стартират както самите разработчици на своите Windows настолни компютри, така и сървърът Gitlab CI под Linux.

За да бъде внедряването на нови тестове оправдано, то не трябва да изисква инсталиране на допълнителни инструменти нито на компютъра на разработчика, нито на сървъра, където тестовете се изпълняват при комитване. Docker решава тази задача.

Тестът трябва да работи на локален сървър по следните причини:

  • Мрежата не е напълно надеждна. От хиляда запитвания едно може да не премине;
    Автоматичният тест в такъв случай няма да мине, работата ще спре, ще трябва да търсите причината в логовете;
  • Твърде честите запитвания не се допускат от някои външни услуги.

Освен това, използването на стенд е нежелателно, защото:

  • Стендът може да бъде повреден не само от лош код, работещ на него, но и от данни, които правилният код не може да обработи;
  • Колкото и да се опитваме да възстановим всички изменения, направени от теста, по време на самия тест, нещо може да се обърка (иначе, защо е тестът?).

За проекта и организацията на процеса

Нашата компания разработи микросервизно уеб приложение, работещо в Docker в облака на Amazon AWS. В проекта вече се използваха юнит тестове, но често възникваха грешки, които юнит тестовете не откриваха. Необходимо беше да се тества целият микросервис заедно с базата данни и услугите на Amazon.

В проекта се прилага стандартен процес на непрекъсната интеграция, включващ тестване на микросервиза при всяко комитиране. След назначаване на задачата разработчикът внася промени в микросервиза, тестува го ръчно и стартира всички налични автоматични тестове. При необходимост разработчикът променя тестовете. Ако не са открити проблеми, се извършва комит в клонa на задачата. След всеки комит на сървъра автоматически се стартират тестове. Сливането в общия клон и стартирането на автоматичните тестове в него се извършва след успешно ревю. Ако тестовете в общия клон преминат, услугата автоматично се обновява в тестовата обстановка на 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 създавате тема и подписвате например SQS опашка към нея. В темата могат да се изпращат съобщения. При това съобщението се изпраща до всяка опашка, подписана на тази тема. В SNS няма метод за четене на съобщения. Ако в процеса на дебъгинг или тестване е нужно да научите какво се изпраща в SNS, можете да създадете SQS опашка, да я подпишете на нужната тема и да четете опашката.

Автоматизирано тестване на микросервизи в Docker за непрекъсната интеграция

API Gateway

Повечето услуги не са достъпни директно от интернет. Достъпът се осъществява чрез API Gateway, който проверява правата за достъп. Това също е наша услуга и за нея също има тестове.

Уведомления в реално време

Приложението използва SignalR, за да показва на потребителя уведомления в реално време. Това е реализирано в услугата за известия. Тя е достъпна директно от интернет и също работи с OAuth, защото вграждането на поддръжка за WebSocket в 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.

За тестването в проекта се използва Postman. Той беше и преди, но го стартираха ръчно и тестват приложението, вече разгрънато на стенда. Този инструмент позволява да се правят произволни HTTP(S) заявки и да се проверява съответствието на отговорите с очакванията. Запитванията се организират в колекция и можете да стартирате цялата колекция изцяло.

Автоматизирано тестване на микросервизи в Docker за непрекъсната интеграция

Как е устроен автоматичният тест

По време на теста в Docker всичко работи: и тестваният сервис, и Postgres, и инструмента за миграция, и Postman, а всъщност, неговата конзолна версия – Newman.

Docker решава редица проблеми:

  • Независимост от конфигурацията на хоста;
  • Инсталиране на зависимости: Docker изтегля образи от Docker Hub;
  • Възстановяване на системата в първоначално състояние: просто изтриваме контейнерите.

Docker-compose обединява контейнерите в виртуална мрежа, изолирана от интернет, в която контейнерите се намират помежду си чрез домейн имена.

Тестът се управлява от shell скрипт. За стартиране на теста под Windows използваме git-bash. По този начин, един скрипт е достатъчен както за Windows, така и за Linux. Git и Docker са инсталирани при всички разработчици в проекта. При инсталиране на Git под Windows, се инсталира и git-bash, така че той също е наличен за всички.

Скриптът изпълнява следните стъпки:

  • Създаване на Docker образи
    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, на базата на който се създава образ, съдържащ скрипта.

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

Проблеми, с които може да се сблъскате

Изчакване на готовност

Когато контейнерът със сървиса е стартиран, това не означава, че е готов да приема връзки. Трябва да изчакаме връзката, за да продължим.

Понякога тази задача се решава с помощта на скрипт wait-for-it.sh, който очаква възможността да установи TCP-съединение. Въпреки това, LocalStack може да върне грешка 502 Bad Gateway. Освен това, той се състои от множество услуги и, ако една от тях е готова, това не означава нищо за останалите.

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

Конфликти на паралелни задачи

Няколко теста могат да работят едновременно на един Docker-хост, затова имената на контейнерите и мрежите трябва да са уникални. Освен това, тестовете от различни клонове на една услуга също могат да работят едновременно, така че няма да е достатъчно просто да укажете различни имена в всеки compose-файл.

Решение: скриптът задава уникална стойност на променливата COMPOSE_PROJECT_NAME.

Спецификации за Windows

Когато използвате Docker в Windows, има няколко неща, на които искам да обърна внимание, тъй като този опит е важен за разбирането на причините за грешките.

  1. Shell-скриптовете в контейнера трябва да имат Linux завършеци на редовете.
    CR символът за shell-а е синтактична грешка. От съобщението за грешка е трудно да се разбере, че проблемът е в това. При редактиране на такива скриптове в 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, и следователно изходът му попада в конзолата. Остава само да направим така, че и изходът на услугата да се появи там.

Първоначалното решение беше да се прави docker-compose up без флага -d, но използвайки възможностите на shell-а, да задействаме този процес във фонов режим:

docker-compose up <service> &

Това работеше, докато не се наложи да се изпращат логовете от Docker към външна услуга. docker-compose up спря да извежда логовете в конзолата. Въпреки това командата работеше docker attach.

Решение:

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, който има готов образ в Docker Hub. Очакваните заявки и отговори на тях се настройват с HTTP заявка. API е документирано, така че правим заявки от Postman.

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

Използваме OAuth и JSON Web Tokens (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-скриптът в Docker файла изглежда така:

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

Команда docker inspect показва за контейнера обикновен статус, здравен статус и код на завършване.

След приключването на Newman, скриптът проверява, че всички контейнери с клиента са приключили, и то с код 0.

Щастието съществува

След като преодоляхме описаните по-горе трудности, разполагаме с набор от стабилно работещи тестове. В тестовете всеки сервиз работи като едно цяло, взаимодействува с базата данни и с Amazon LocalStack.

Тези тестове защитават екипа от 30+ разработчици от грешки в приложението с комплексно взаимодействие на 10+ микросервиза при чести разгръщания.

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster