I projekter relateret til udvikling af mikrotjenestearkitektur bliver CI/CD en akut nødvendighed snarere end en god ide. Automatisk testning er en uundgåelig del af kontinuerlig integration, og en velovervejet tilgang kan give teamet mange hyggelige aftener med familie og venner. Ellers risikerer projektet aldrig at blive færdigt.
Man kan dække hele koden i mikrotjenesten med unit-tests ved hjælp af mock-objekter, men det løser kun opgaven delvist og efterlader mange spørgsmål og kompleksiteter, især når det gælder testning af databehandling. Som altid er de mest presserende – testning af datakonsistens i relationelle databaser, testning med cloud-tjenester og forkerte antagelser ved skrivning af mock-objekter.
Alt dette og lidt mere løses ved at teste hele mikrotjenesten i en Docker-container. En ubestridelig fordel ved at sikre gyldigheden af testene er, at testene udføres på præcis de samme Docker-billeder, der går i produktion.
Automatisering af sådan en tilgang præsenterer en række problemer, hvis løsninger vil blive beskrevet lidt senere:
- konflikter mellem parallelle opgaver på en Docker-host;
- konflikter med identifikatorer i databasen under testiterationer;
- ventetid på mikrotjenesternes tilgængelighed;
- sammenlægning og udtrækning af logs til eksterne systemer;
- testning af udgående HTTP-anmodninger;
- testning af websockets (ved hjælp af SignalR);
- testning af OAuth-autentifikation og autorisation.
Dette er en artikel inspireret af på SECR 2019. Så til dem, der ikke orker at læse, .

I artiklen vil jeg beskrive, hvordan man med et script kan starte den testede service, databasen og Amazon AWS-tjenesterne i Docker, derefter køre testene i Postman, og efter deres afslutning stoppe og fjerne de oprettede containere. Testene køres ved hver kodeændring. På den måde sikrer vi, at hver version korrekt arbejder med databasen og AWS-tjenesterne.
Det samme script køres af både udviklerne på deres Windows-desktops og Gitlab CI-serveren under Linux.
For at implementeringen af nye tests skal være berettiget, må den ikke kræve installation af yderligere værktøjer hverken på udviklerens computer eller på serveren, hvor testene kører ved commit. Docker løser dette problem.
Testet skal køre på en lokal server af følgende grunde:
- Netværket er aldrig absolut pålideligt. Ud af tusind forespørgsler kan én muligvis ikke gå igennem;
En automatisk test vil i så fald ikke gå igennem, arbejdet stopper, og man må finde årsagen i loggene; - For hyppige forespørgsler er ikke tilladt af nogle tredjeparts tjenester.
Derudover er det uønsket at anvende en stand af følgende årsager:
- Ikke kun dårlig kode, der kører på standen, kan nedbryde den, men også data, som den rette kode ikke kan håndtere;
- Uanset hvor meget vi forsøger at tilbageføre alle ændringer foretaget af testen under selve testen, kan noget gå galt (ellers, hvorfor skulle vi teste?).
Om projektet og organisationen af processen
Vores virksomhed har udviklet en mikrotjeneste webapplikation, der kører i Docker i Amazon AWS. I projektet blev der allerede anvendt enhedstests, men der opstod ofte fejl, som enhedstests ikke opdagede. Det var nødvendigt at teste hele mikrotjenesten sammen med databasen og Amazon-tjenesterne.
Projektet anvender en standard proces for kontinuerlig integration, der inkluderer test af mikrotjenesten ved hver commit. Efter oprettelsen af opgaven foretager udvikleren ændringer i mikrotjenesten, tester den manuelt og kører alle tilgængelige automatiserede tests. Hvis nødvendigt ændrer udvikleren testene. Hvis der ikke opdages problemer, foretages der et commit i denne opgaves gren. Efter hver commit kører tests automatisk på serveren. Merget til den fælles gren og udførelsen af automatiserede tests på den sker efter en vellykket gennemgang. Hvis testene på den fælles gren er bestået, opdateres tjenesten automatisk i testmiljøet på Amazon Elastic Container Service (standen). Standen er nødvendig for alle udviklere og testere, og det er uønsket at nedbryde den. Testere kontrollerer fix eller ny funktion i dette miljø ved at udføre manuelle tests.
Projektets arkitektur

Applikationen består af mere end ti tjenester. Nogle af dem er skrevet i .NET Core, mens andre er skrevet i NodeJs. Hver tjeneste kører i en Docker-container i Amazon Elastic Container Service. Hver har sin egen Postgres-database, og nogle har endda Redis. Der er ingen delte databaser. Hvis flere tjenester har brug for de samme data, sendes de data, når de ændres, til hver af disse tjenester via SNS (Simple Notification Service) og SQS (Amazon Simple Queue Service), og tjenesterne gemmer dem i deres egne separate databaser.
SQS og SNS
SQS giver mulighed for at sende beskeder til en kø og læse beskeder fra køen via HTTPS-protokollen.
Hvis flere tjenester læser fra én kø, modtager hver besked kun én af dem. Dette er nyttigt, når man kører flere instanser af en tjeneste for at fordele belastningen imellem dem.
Hvis det er nødvendigt, at hver besked leveres til flere tjenester, skal hver modtager have sin egen kø, og til at duplikere beskeder til flere køer kræves SNS.
I SNS opretter du et emne og abonnerer for eksempel på en SQS-kø. Du kan sende beskeder til emnet. Når du sender en besked til emnet, sendes beskeden til hver kø, der er tilmeldt dette emne. Der er ikke nogen metode i SNS til at læse beskeder. Hvis du under fejlfinding eller test har brug for at vide, hvad der sendes til SNS, kan du oprette en SQS-kø, tilmelde den det relevante emne og læse køen.

API Gateway
De fleste tjenester er ikke tilgængelige direkte fra internettet. Adgang foregår gennem API Gateway, som kontrollerer adgangsrettighederne. Det er også vores tjeneste, og der er tests til den.
Real-time notifikationer
Applikationen bruger , for at vise brugeren real-time notifikationer. Dette er implementeret i notifikationsservicen. Den er tilgængelig direkte fra internettet og arbejder selv med OAuth, fordi det viste sig at være uhensigtsmæssigt at integrere WebSockets i Gateway'en i forhold til integrationen af OAuth og notifikationsservicen.
En velkendt tilgang til testning
Enhedstest underlægger mock-objekter til sådanne ting som databasen. Hvis mikroservicen for eksempel forsøger at oprette en post i en tabel med en fremmed nøgle, og posten som denne nøgle refererer til ikke eksisterer, kan forespørgslen ikke udføres. Enhedstestene kan ikke opdage dette.
I foreslår at bruge en in-memory database og injicere mock-objekter.
In-memory databasen – dette er en af de databaser, som Entity Framework understøtter. Den er udviklet specifikt til testformål. Data i en sådan database gemmes kun indtil afslutningen af den proces, der bruger den. Der er ikke behov for at oprette tabeller, og dataintegriteten kontrolleres ikke.
Mock-objekter simulerer det substituerede klasse kun i det omfang, testudvikleren forstår dens funktion.
Hvordan man får Postgres til at starte automatisk og udføre migrationen ved testopstart, er ikke beskrevet i artiklen fra Microsoft. Min løsning gør det, og desuden tilføjes der ikke nogen kode til mikroservicen specifikt til testformål.
Lad os gå videre til løsningen
I udviklingsprocessen blev det klart, at enhedstest ikke var tilstrækkeligt til at opdage alle problemer til tiden, så det blev besluttet at tilgå spørgsmålet fra en anden vinkel.
Opsætning af testmiljøet
Den første opgave – at oprette testmiljøet. Trinene der er nødvendige for at starte mikroservicen:
- Konfigurere den testede service til det lokale miljø, hvor forbindelsesdetaljerne til databasen og AWS angives i miljøvariablerne;
- Starte Postgres og udføre migration ved at køre Liquibase.
I relationelle databaser skal dataindsætning ske efter oprettelsen af dataskemaet, simpelthen sagt, tabellerne. Ved opdatering af applikationen skal tabellerne bringes til den form, der anvendes i den nye version, gerne uden datatab. Dette kaldes migration. Oprettelse af tabeller i en oprindeligt tom database er et specifikt tilfælde af migration. Migration kan integreres i selve applikationen. Både i .NET og NodeJS findes der rammer til migration. I vores tilfælde har mikroservices for sikkerhedens skyld ikke ret til at ændre dataskemaet, og migrationen udføres ved hjælp af Liquibase. - Starte Amazon LocalStack. Dette er en implementering af AWS-tjenester til lokal kørsel. Der findes et klarlagt billede af LocalStack på Docker Hub.
- Køre scriptet for at oprette de nødvendige enheder i LocalStack. Shell-scripts bruger AWS CLI.
Til testning i projektet anvendes . Det var der allerede, men det blev kørt manuelt og testet på applikationen, der allerede var implementeret på testmiljøet. Dette værktøj gør det muligt at foretage vilkårlige HTTP(S)-anmodninger og kontrollere, om svarene stemmer overens med forventningerne. Anmodningerne samles i en samling, og hele samlingen kan køres på én gang.

Hvordan den automatiske test fungerer
Under test, everything works in Docker: both the service being tested and Postgres, and the migration tool, and Postman, or rather its console version - Newman.
Docker addresses a number of problems:
- Independence from host configuration;
- Dependency installation: Docker pulls images from Docker Hub;
- Returning the system to its original state: simply delete the containers.
Docker-compose it combines containers into a virtual network isolated from the internet, where containers discover each other by domain names.
The test is managed by a shell script. To run the test on Windows, we use git-bash. Thus, one script is sufficient for both Windows and Linux. Git and Docker are installed for all developers on the project. When Git is installed on Windows, git-bash is also installed, so everyone has it.
The script performs the following steps:
- Building Docker images
docker-compose build - Starting the DB and LocalStack
docker-compose up -d - Migrating the DB and preparing LocalStack
docker-compose run - Starting the service under test
docker-compose up -d - Running the test (Newman)
- Stopping all containers
docker-compose down - Posting results to Slack
We have a chat where messages with a green checkmark or a red cross and a link to the log appear.
These steps involve the following Docker images:
- The service being tested is the same image as for production. The test configuration is via environment variables.
- For Postgres, Redis, and LocalStack, ready-made images from Docker Hub are used. There are also ready-made images for Liquibase and Newman. We build our own on their basis, adding our files.
- For preparing LocalStack, a ready-made AWS CLI image is used, and on its basis, an image containing the script is created.
Ved at bruge , you don't need to build a Docker image just to add files to the container. However, volumes are not suitable for our environment because Gitlab CI tasks run in containers themselves. You can manage Docker from such a container, but volumes only mount folders from the host system, not from another container.
Problems you might encounter
Waiting for readiness
When the container with the service is started, it doesn't mean it's ready to accept connections. You need to wait for the connection to continue.
This task is sometimes solved using a script , som venter på muligheden for at oprette en TCP-forbindelse. Dog kan LocalStack returnere en fejl 502 Bad Gateway. Desuden består det af mange tjenester, og hvis én af dem er klar, siger det ingenting om de øvrige.
Løsning: forberedelsesscripts til LocalStack, der venter på et 200-svar fra både SQS og SNS.
Konflikter mellem parallelle opgaver
Flere tests kan køre samtidig på én Docker-vært, så navne på containere og netværk skal være unikke. Desuden kan tests fra forskellige grene af den samme tjeneste også køre samtidigt, så det er ikke tilstrækkeligt bare at angive sine egne navne i hver compose-fil.
Løsning: scriptet tildeler en unik værdi til variablen COMPOSE_PROJECT_NAME.
Muligheder ved brug af Windows
Når man bruger Docker på Windows, er der flere ting, jeg gerne vil gøre opmærksom på, da denne oplevelse er vigtig for at forstå årsagerne til fejl.
- Shell-scripts i containeren skal have Linux-venlige linjeskift.
CR-tegn for shell er en syntaksfejl. Fejlmeddelelsen gør det svært at forstå, at det er dette, der er problemet. Ved redigering af sådanne scripts i Windows er det nødvendigt med den rette tekstbehandler. Desuden skal versionskontrolsystemet være korrekt konfigureret.
Sådan konfigureres git:
git config core.autocrlf input- Git-bash emulerer standardmapper fra Linux, og ved kald af en exe-fil (inklusive docker.exe) erstatter den absolutte Linux-stier med Windows-stier. Dette giver dog ikke mening for stier, der ikke er på den lokale maskine (eller stier i containeren). Denne opførsel kan ikke deaktiveres.
Løsning: tilføje en ekstra skråstreg i begyndelsen af stien: \/\/bin i stedet for \/bin. Linux forstår sådanne stier, for den betragter flere skråstreger som en. Men git-bash genkender ikke sådanne stier og forsøger ikke at konvertere dem.
Loguddata
Når tests udføres, vil jeg gerne se logfiler fra både Newman og den testede tjeneste. Da hændelserne i disse logfiler hænger sammen, er det meget mere praktisk at samle dem i én konsol i stedet for at have to separate filer. Newman startes via docker-compose run, og derfor sendes dets output til konsollen. Det resterer at sikre, at også tjenestens output når derind.
Den første løsning var at gøre docker-compose up uden flaget -d, men ved at bruge shell-funktioner sende denne proces til baggrunden:
docker-compose up <service> &Dette virkede, indtil det blev nødvendigt at sende logs fra Docker til en ekstern tjeneste. docker-compose up stop med at udskrive logs til konsollen. Men kommandoen fungerede docker attach.
Løsning:
docker attach --no-stdin ${COMPOSE_PROJECT_NAME}__1 &Konflikt med identifikatorer ved testiterationer
Testene køres med flere iterationer. Databasen tømmes ikke. Poster i databasen har unikke ID'er. Hvis der skrives specifikke ID'er i forespørgslerne, vil vi få en konflikt i den anden iteration.
For at undgå det skal enten ID'erne være unikke, eller alle objekter, der er oprettet af testen, skal slettes. Nogle objekter kan ikke slettes i henhold til kravene.
Løsning: generer GUID'er med scripts i Postman.
var uuid = require('uuid');
var myid = uuid.v4();
pm.environment.set('myUUID', myid);Derefter bruges symbolet i forespørgslen {{myUUID}}, som bliver erstattet med værdien af variablen.
Interaktion via LocalStack
Hvis den testede service læser fra en SQS-kø eller skriver til den, skal testen også arbejde med denne kø for at kontrollere det.
Løsning: forespørgsler fra Postman til LocalStack.
AWS-service API'er er dokumenteret, hvilket muliggør forespørgsler uden SDK.
Hvis servicen skriver til køen, læser vi den og kontrollerer indholdet af meddelelsen.
Hvis servicen sender meddelelser til SNS, oprettes der også en kø og tilmeldes dette SNS-emne i forberedelsesfasen for LocalStack. Alt reduceres til det, der er beskrevet ovenfor.
Hvis servicen skal læse en meddelelse fra køen, skriver vi denne meddelelse til køen i det forrige trin i testen.
Test af HTTP-forespørgsler, der udsendes af den testede mikrotjeneste
Nogle tjenester arbejder via HTTP med noget andet end AWS, og nogle AWS-funktioner er ikke implementeret i LocalStack.
Løsning: i disse tilfælde kan hjælpe, som har et klart billede i . Forventede forespørgsler og svar på disse konfigureres med en HTTP-forespørgsel. API'er er dokumenteret, så vi laver forespørgsler fra Postman.
Test af OAuth-autentifikation og autorisation
Vi bruger OAuth og . Til testen har vi brug for en OAuth-udbyder, som vi kan køre lokalt.
Al interaktion mellem servicen og OAuth-udbyderen opsummeres i to forespørgsler: først anmodes der om konfigurationen /.well-known/openid-configuration, og derefter anmodes der om den offentlige nøgle (JWKS) på adressen fra konfigurationen. Alt dette er statisk indhold.
Løsning: vores test-OAuth-udbyder er en server for statisk indhold og to filer på den. Tokenet er genereret én gang og committet til Git.
Særlige forhold ved test af SignalR
Postman fungerer ikke med WebSockets. Der er udviklet et specielt værktøj til test af SignalR.
SignalR-klienten kan være mere end bare en browser. Der findes et klientbibliotek til .NET Core. Klienten, der er skrevet i .NET Core, etablerer forbindelse, gennemgår autentificering og venter på en bestemt sekvens af meddelelser. Hvis der modtages en uventet meddelelse, eller forbindelsen afbrydes, afsluttes klienten med kode 1. Ved modtagelse af den sidste forventede meddelelse afsluttes den med kode 0.
Samtidig kører Newman som klient. Flere klienter startes for at sikre, at meddelelserne leveres til alle, der skal have dem.

For at starte flere klienter bruges indstillingen —scale i docker-compose kommandolinjen.
Før lanceringen venter Postman-scriptet på, at alle klienter etablerer forbindelse.
Vi har allerede oplevet problemet med at vente på forbindelsen. Her var det serverne, men her er det klienten. En anden tilgang er nødvendig.
Løsning: klienten i containeren bruger mekanismen , for at informere scriptet på værten om sin status. Klienten opretter en fil på en bestemt sti, f.eks. /healthcheck, så snart forbindelsen er etableret. HealthCheck-scriptet i Docker-filen ser sådan ud:
HEALTHCHECK --interval=3s CMD if [ ! -e /healthcheck ]; then false; fiKommandoen docker inspect viser den almindelige status for containeren, sundhedsstatus og exitkode.
Efter Newman afsluttes, kontrollerer scriptet, at alle containere med klienten er afsluttet, og at de er afsluttet med kode 0.
Der er glæde
Efter at have overvundet de nævnte udfordringer har vi nu et sæt stabilt fungerende tests. I testene fungerer hver tjeneste som en helhed, interagerer med databasen og med Amazon LocalStack.
Disse tests beskytter et team på 30+ udviklere mod fejl i applikationen med komplekse interaktioner mellem 10+ mikrotjenester ved hyppige udrulninger.
Kilde: habr.com
