I projekt som rör utveckling av mikrotjänstarkitektur går CI/CD från att vara en trevlig möjlighet till en akut nödvändighet. Automatiserad testning är en oumbärlig del av kontinuerlig integration, och en väl genomtänkt strategi kan ge teamet många trevliga kvällar med familj och vänner. Annars riskerar projektet att aldrig bli slutfört.
Det är möjligt att täcka hela koden för mikrotjänsten med enhetstester med hjälp av mock-objekt, men detta löser endast delvis problemet och lämnar många frågor och svårigheter, särskilt vid testning av databehandling. Som alltid är de mest akuta – testning av datakonsistens i en relationsdatabas, testning av interaktioner med molntjänster och felaktiga antaganden vid skrivning av mock-objekt.
Allt detta och lite mer löses genom att testa hela mikrotjänsten i en Docker-container. En klar fördel för att säkerställa giltigheten av testerna är att samma Docker-bilder som går i produktion utsätts för tester.
Automatiseringen av ett sådant tillvägagångssätt presenterar ett antal problem, vars lösningar kommer att beskrivas strax nedan:
- konflikter mellan parallella uppgifter på en och samma Docker-värd;
- identifieringskonflikter i databasen vid testiterationer;
- väntan på att mikrotjänsterna ska bli klara;
- sammanställning och utskrift av loggar till externa system;
- testning av utgående HTTP-förfrågningar;
- testning av websockets (med hjälp av SignalR);
- testning av autentisering och auktorisering med OAuth.
Detta är en artikel inspirerad av på SECR 2019. Så för dem som är lata och inte vill läsa, .

I artikeln kommer jag att berätta hur man med hjälp av ett skript kan starta den testade tjänsten, databasen och Amazon AWS-tjänster i Docker, sedan köra tester i Postman och efter deras slutförande stoppa och ta bort de skapade containrarna. Tester utförs vid varje kodändring. På så sätt säkerställer vi att varje version fungerar korrekt med databasen och AWS-tjänsterna.
Det är samma skript som används både av utvecklarna på sina Windows-datorer och av Gitlab CI-servern som körs under Linux.
För att implementeringen av nya tester ska vara berättigad får den inte kräva installation av ytterligare verktyg varken på utvecklarens dator eller på servern där testerna körs vid commit. Docker löser detta problem.
Testet bör köras på en lokal server av följande skäl:
- Nätverket är aldrig helt pålitligt. Av tusen förfrågningar kan en misslyckas;
I så fall kommer det automatiska testet att misslyckas, arbetet stannar, och man måste leta efter orsaken i loggarna; - För många frekventa förfrågningar tillåts inte av vissa externa tjänster.
Dessutom är det oönskat att involvera en testmiljö eftersom:
- Testmiljön kan förstöras inte bara av dålig kod som körs på den, utan även av data som korrekt kod inte kan bearbeta;
- Hur mycket vi än försöker återställa alla ändringar som gjorts av testet, kan något gå fel under själva testet (annars, varför testa?).
Om projektet och processen
Vårt företag har utvecklat en mikrotjänstbaserad webbapplikation som körs i Docker på Amazon AWS. I projektet användes redan enhetstester, men det uppstod ofta fel som enhetstesterna inte upptäckte. Det var nödvändigt att testa hela mikrotjänsten tillsammans med databasen och Amazons tjänster.
Projektet följer en standardprocess för kontinuerlig integration som inkluderar testning av mikrotjänsten vid varje commit. Efter att en uppgift tilldelats gör utvecklaren ändringar i mikrotjänsten, testar den manuellt och kör alla existerande automatiska tester. Vid behov justerar utvecklaren testerna. Om inga problem upptäcks görs en commit i grenen för denna uppgift. Efter varje commit körs automatiskt tester på servern. Sammanfogning till huvudgrenen och körning av automatiska tester där sker efter en framgångsrik granskning. Om testerna på huvudgrenen går igenom, uppdateras tjänsten automatiskt i testmiljön på Amazon Elastic Container Service (testmiljön). Testmiljön är nödvändig för alla utvecklare och testare, och det är oönskat att förstöra den. Testare på denna miljö verifierar en fix eller en ny funktion genom att utföra manuella tester.
Projektarkitektur

Applikationen består av mer än tio tjänster. Vissa av dem är skrivna i .NET Core, medan andra är skrivna i NodeJs. Varje tjänst körs i en Docker-container i Amazon Elastic Container Service. Varje tjänst har sin egen Postgres-databas, och vissa har även Redis. Det finns inga gemensamma databaser. Om flera tjänster behöver samma data, överförs dessa data till varje tjänst vid förändring via SNS (Simple Notification Service) och SQS (Amazon Simple Queue Service), och tjänsterna sparar dem i sina separata databaser.
SQS och SNS
SQS gör det möjligt att via HTTPS lägga meddelanden i en kö och läsa meddelanden från kön.
Om flera tjänster läser från en kö, kommer varje meddelande endast att komma till en av dem. Detta är användbart när flera instanser av samma tjänst körs för att fördela belastningen mellan dem.
Om det är nödvändigt att varenda meddelande ska levereras till flera tjänster, måste varje mottagare ha sin egen kö, och för att duplicera meddelanden i flera köer krävs SNS.
I SNS skapar du ett topic och prenumererar exempelvis en SQS-kö på det. Meddelanden kan skickas till topicen. Då skickas meddelandet till varje kö som är prenumererad på detta topic. Det finns ingen metod i SNS för att läsa meddelanden. Om du i debug- eller testningsprocessen vill ta reda på vad som skickas till SNS, kan du skapa en SQS-kö, prenumerera den på den relevanta topicen och läsa kön.

API Gateway
De flesta tjänster är inte direkt tillgängliga från internet. Åtkomsten sker via API Gateway, som kontrollerar rättigheterna. Detta är också vår tjänst och det finns även tester för detta.
Meddelanden i realtid
Applikationen använder , för att visa användaren meddelanden i realtid. Detta implementeras i meddelandetjänsten. Den är direkt tillgänglig från internet och fungerar själv med OAuth, eftersom det visade sig vara oklokt att integrera stöd för WebSockets i Gateway jämfört med integreringen av OAuth och meddelandetjänsten.
Ett känt tillvägagångssätt för testning
Enhetstester ersätter sådana saker som databaser med mock-objekt. Om en mikrotjänst, till exempel, försöker skapa en post i en tabell med en utländsk nyckel, och posten som denna nyckel hänvisar till inte existerar, kan förfrågan inte genomföras. Enhetstester kan inte upptäcka detta.
I föreslår att använda en in-memory databas och injicera mock-objekt.
In-memory-databasen är en av de databashanterare som stöds av Entity Framework. Den är speciellt skapad för tester. Data i en sådan databas lagras endast tills processen som använder den avslutas. Det behöver inte skapas tabeller i den, och dataintegritet kontrolleras inte.
Mock-objekt modellerar den ersättande klassen endast så mycket som testutvecklaren förstår hur den fungerar.
Hur man får Postgres att starta automatiskt och genomföra migrationer vid teststart nämns inte i artikeln från Microsoft. Min lösning gör detta och lägger dessutom inte till någon kod i mikrotjänsten specifikt för tester.
Vi går vidare till lösningen
Under utvecklingen blev det tydligt att enhetstester inte var tillräckliga för att i tid hitta alla problem, så det beslutades att närma sig detta från ett annat håll.
Återställning av testmiljön
Det första steget är att sätta upp testmiljön. Stegen som behövs för att starta mikrotjänsten är:
- Ställ in den testade tjänsten på lokal miljö, där anslutningsuppgifterna till databasen och AWS anges i miljövariabler;
- Starta Postgres och genomför migration genom att köra Liquibase.
I relationella databashanterare måste man innan man skriver data till databasen skapa en databasstruktur, enklare sagt, tabeller. Vid uppdatering av applikationen måste tabellerna anpassas till den nya versionens format, helst utan dataförlust. Detta kallas migration. Att skapa tabeller i en ursprungligen tom databas är ett specialfall av migration. Migration kan integreras i själva applikationen. Både i .NET och NodeJS finns det ramverk för migration. I vårt fall, av säkerhetsskäl, har mikrotjänsterna inte rätt att ändra databasstrukturen, och migrationen utförs med hjälp av Liquibase. - Starta Amazon LocalStack. Detta är en implementation av AWS-tjänster för att köra lokalt. För LocalStack finns det en färdig bild på Docker Hub.
- Kör skriptet för att skapa nödvändiga enheter i LocalStack. Shell-skript använder AWS CLI.
För testning på projektet används . Den har funnits tidigare, men den startades manuellt och testade applikationen som redan var distribuerad på servern. Detta verktyg gör det möjligt att göra godtyckliga HTTP(S)-begärningar och kontrollera att svaren överensstämmer med förväntningarna. Begärningarna grupperas i en samling, och hela samlingen kan köras.

Hur automatiska tester fungerar
Under test in Docker, everything works: both the service being tested, and Postgres, and the migration tool, and Postman, or rather, its console version – Newman.
Docker addresses a number of issues:
- Independence from host configuration;
- Dependency installation: Docker downloads images from Docker Hub;
- Returning the system to its original state: simply remove the containers.
Docker-compose combines containers into a virtual network isolated from the internet, where containers find 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 installing Git 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 database and LocalStack
docker-compose up -d - Database migration and LocalStack setup
docker-compose run - Starting the tested service
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 red cross and a link to the log come in.
The following Docker images are involved in these steps:
- The tested service uses the same image as production. The configuration for testing is through environment variables.
- Ready-made images from Docker Hub are used for Postgres, Redis, and LocalStack. There are also ready-made images for Liquibase and Newman. We build our images on top of theirs, adding our files.
- For setting up LocalStack, a ready-made AWS CLI image is used, and based on it, an image containing the script is created.
Genom att använda , it is not necessary to build a Docker image just to add files to the container. However, volumes are not suitable for our environment, because Gitlab CI tasks themselves operate in containers. From such a container, Docker can be managed, but volumes mount folders only from the host system, not from another container.
Problems that can be encountered
Waiting for readiness
When the service container is running, it doesn’t mean it is ready to accept connections. You need to wait for the connection to proceed.
This task is sometimes solved with a script , som väntar på att få etabliera en TCP-anslutning. Men LocalStack kan ge ett felmeddelande 502 Bad Gateway. Dessutom består det av många tjänster, och om en av dem är klar betyder det ingenting för de andra.
Lösning: förberedelsesskript för LocalStack som väntar på 200-svar både från SQS och SNS.
Konflikter mellan parallella uppgifter
Flera tester kan köras samtidigt på en Docker-värd, så kontainernamn och nätverksnamn måste vara unika. Dessutom kan tester från olika grenar av en tjänst också köras samtidigt, så att bara ange sina namn i varje compose-fil räcker inte.
Lösning: skriptet sätter ett unikt värde för variabeln COMPOSE_PROJECT_NAME.
Egenskaper för Windows
När man använder Docker i Windows finns det flera saker som jag vill uppmärksamma, eftersom denna upplevelse är viktig för att förstå orsakerna till felen.
- Shell-skript i containern måste ha Unix-liknande radslut.
CR-tecknet för shell är en syntaxfel. Utifrån felmeddelandet är det svårt att förstå att det handlar om detta. När man redigerar sådana skript i Windows behövs en korrekt textredigerare. Dessutom måste versionskontrollsystemet vara korrekt inställt.
Så här ställer du in git:
git config core.autocrlf input- Git-bash emulerar standardmappar för Linux och när exe-filer (inklusive docker.exe) anropas konverterar den absoluta Linux-sökvägar till Windows-sökvägar. Men detta har ingen betydelse för sökvägar som inte finns på den lokala maskinen (eller sökvägar i containern). Detta beteende kan inte stängas av.
Lösning: att lägga till en extra snedstreck i början av sökvägen: \/bin istället för \/bin. Linux förstår sådana sökvägar, för det innebär flera snedstreck samma sak som ett. Men git-bash känner inte igen sådana sökvägar och försöker inte omvandla dem.
Loggutdata
Vid körning av tester vill jag se loggar både från Newman och från den testade tjänsten. Eftersom händelserna i dessa loggar är kopplade till varandra är det mycket mer praktiskt att kombinera dem i en konsol jämfört med två separata filer. Newman körs via docker-compose run, och därför hamnar dess utdata i konsolen. Det återstår att få utdata från tjänsten att också hamna där.
Den ursprungliga lösningen var att göra docker-compose up utan flaggan -d, men genom att använda shell-funktioner skicka denna process till bakgrunden:
docker-compose up <service> &Det fungerade tills det blev nödvändigt att skicka loggar från Docker till en extern tjänst. docker-compose up slutade skriva loggar till konsolen. Men kommandot fungerade docker attach.
Lösning:
docker attach --no-stdin ${COMPOSE_PROJECT_NAME}__1 &Konflikt med identifierare under testiterationer
Tester körs med flera iterationer. Databasen rensas inte under tiden. Poster i databasen har unika ID. Om man anger specifika ID i förfrågningarna, får vi en konflikt vid andra iterationen.
För att undvika det måste ID vara unika, eller så måste alla objekt skapade av testet raderas. Vissa objekt kan inte raderas, i enlighet med kraven.
Lösning: generera GUID:ar med skript i Postman.
var uuid = require('uuid');
var myid = uuid.v4();
pm.environment.set('myUUID', myid);Använd sedan symbolen i förfrågan {{myUUID}}, som kommer att ersättas med värdet för variabeln.
Interaktion via LocalStack
Om den testade tjänsten läser från SQS-kö eller skriver till den, måste testet också arbeta med den kön.
Lösning: förfrågningar från Postman till LocalStack.
API:erna för AWS-tjänster är dokumenterade, vilket gör att vi kan göra förfrågningar utan SDK.
Om tjänsten skriver till kön, läser vi den och kontrollerar innehållet i meddelandet.
Om tjänsten skickar meddelanden till SNS, skapas också en kö och prenumereras på detta SNS-ämne under LocalStacks förberedelse. Allt summeras till det som beskrevs ovan.
Om tjänsten ska läsa ett meddelande från kön, skriver vi meddelandet i kön under det föregående steget av testet.
Testa HTTP-förfrågningar som kommer från den testade mikrotjänsten
Vissa tjänster arbetar via HTTP med något annat än AWS, och vissa AWS-funktioner är inte implementerade i LocalStack.
Lösning: i dessa fall kan , som har en färdig bild i . Förväntade förfrågningar och svar på dem ställs in med en HTTP-förfrågan. API:et är dokumenterat, så vi gör förfrågningar från Postman.
Testa autentisering och auktorisering med OAuth
Vi använder OAuth och . För testet behövs en OAuth-leverantör som vi kan köra lokalt.
All interaktion mellan tjänsten och OAuth-leverantören sammanfattas i två förfrågningar: först begärs konfigurationen /.well-known/openid-configuration, och sedan begärs den offentliga nyckeln (JWKS) från adressen i konfigurationen. Allt detta är statiskt innehåll.
Lösning: vår test-OAuth-leverantör är en server för statiskt innehåll och två filer på den. Token genererades en gång och committades i Git.
Specifika egenskaper vid testning av SignalR
Websocket-funktionen fungerar inte med Postman. För att testa SignalR skapades ett särskilt verktyg.
SignalR-klienten behöver inte bara vara en webbläsare. Det finns en klientbibliotek för .NET Core. Klienten, som är skriven i .NET Core, etablerar en anslutning, autentiserar sig och väntar på en viss sekvens av meddelanden. Om ett oväntat meddelande tas emot eller anslutningen bryts, avslutas klienten med kod 1. Vid mottagandet av det sista förväntade meddelandet avslutas den med kod 0.
Samtidigt arbetar Newman som klient. Flera klienter startas för att kontrollera att meddelandena levereras till alla som behöver dem.

För att starta flera klienter används alternativet —scale i kommandoraden för docker-compose.
Innan Postman-skriptet körs, väntar det på att alla klienter ska etablera anslutningen.
Problemet med väntan på anslutningen har vi redan stött på. Men där var det servrar, och här är det klienter. En annan metod behövs.
Lösning: klienten i containern använder mekanismen , för att rapportera skriptet på värden om sin status. Klienten skapar en fil på en viss sökväg, som till exempel \/healthcheck, så snart anslutningen är upprättad. HealthCheck-skriptet i dockerfilen ser ut så här:
HEALTHCHECK --interval=3s CMD if [ ! -e \/healthcheck ]; then false; fiTeam docker inspect visar för containern den vanliga statusen, hälsostatus och exitkod.
Efter att Newman har avslutats, kontrollerar skriptet att alla containrar med klienten har avslutats, och det med kod 0.
Lycka finns
När vi har övervunnit ovanstående svårigheter har vi nu ett antal stabila tester som fungerar. I testerna fungerar varje tjänst som en enhet och interagerar med databasen och Amazon LocalStack.
Dessa tester skyddar ett team av 30+ utvecklare från fel i applikationen med komplex interaktion mellan 10+ mikrotjänster vid frekventa distributioner.
Källa: habr.com
