În proiectele legate de dezvoltarea arhitecturii microservicilor, CI/CD trece de la o posibilitate plăcută la o necesitate acută. Testarea automată este o parte esențială a integrării continue, iar o abordare bine gândită poate oferi echipei multe seri plăcute alături de familie și prieteni. În caz contrar, proiectul riscă să nu fie niciodată finalizat.
Se poate acoperi întreg codul microserviciului cu teste unitare folosind obiecte mock, dar aceasta rezolvă doar parțial problema și lasă multe întrebări și dificultăți, mai ales în ceea ce privește testarea interacțiunii cu datele. Ca de obicei, cele mai acute sunt testarea consistenței datelor în baza de date relațională, testarea interacțiunii cu serviciile cloud și presupunerile greșite în scrierea obiectelor mock.
Toate acestea și puțin mai mult se rezolvă prin testarea întregului microserviciu într-un container Docker. Un avantaj indiscutabil pentru asigurarea valabilității testelor este că aceleași imagini Docker care sunt utilizate în producție sunt supuse testării.
Automatizarea unei astfel de abordări prezintă o serie de probleme, al căror rezolvare va fi descrisă mai jos:
- conflicte între sarcini paralele pe un singur gazdă Docker;
- conflicte de identificare în baza de date în timpul iterațiilor testului;
- așteptarea pregătirii microservicelor;
- unificarea și redirecționarea jurnalelor către sisteme externe;
- testarea cererilor HTTP ieșitoare;
- testarea websocket-urilor (cu SignalR);
- testarea autentificării și autorizării OAuth.
Acesta este un articol bazat pe de la SECR 2019. Așa că pentru cei care nu au răbdare să citească, .

În articol voi explica cum să rulați un serviciu testat, baza de date și serviciile Amazon AWS în Docker printr-un script, apoi să rulați testele pe Postman și, după terminarea lor, să opriți și să ștergeți containerele create. Testele sunt executate la fiecare modificare de cod. Astfel, ne asigurăm că fiecare versiune funcționează corect cu baza de date și serviciile AWS.
Același script este rulat atât de dezvoltatori pe desktopurile lor Windows, cât și de serverul Gitlab CI pe Linux.
Pentru ca implementarea noilor teste să fie justificată, nu ar trebui să necesite instalarea de instrumente suplimentare pe computerul dezvoltatorului sau pe serverul unde testele sunt rulate la commit. Docker rezolvă această problemă.
Testul trebuie să funcționeze pe un server local din următoarele motive:
- Rețelele nu sunt niciodată complet fiabile. Dintr-o mie de cereri, una poate să nu treacă;
În acest caz, testul automat nu va trece, iar procesul se va opri, va trebui să căutăm cauza în jurnale; - Cereri prea frecvente nu sunt acceptate de unele servicii externe.
În plus, utilizarea unui stand este nerecomandată, deoarece:
- Un stand poate fi distrus nu doar de codul defectuos care rulează pe el, ci și de datele pe care codul corect nu le poate procesa;
- Oricât ne-am strădui să revenim asupra tuturor modificărilor făcute de test, în timpul testului, ceva poate merge prost (altfel, de ce ar fi un test?).
Despre proiect și organizarea procesului
Compania noastră a dezvoltat o aplicație web microservică, care funcționează în Docker în cloudul Amazon AWS. Pe proiect au fost deja utilizate teste unitare, însă frecvent au apărut erori pe care testele unitare nu le detectau. Era necesară testarea întregului microserviciu împreună cu baza de date și serviciile Amazon.
Pe proiect se aplică un proces standard de integrare continuă, care include testarea microserviciului la fiecare commit. După atribuirea sarcinii, dezvoltatorul face modificări în microserviciu, îl testează manual și rulează toate testele automate existente. Dacă este necesar, dezvoltatorul modifică testele. Dacă nu sunt detectate probleme, se face commit în ramura sarcinii respective. După fiecare commit, testele sunt rulate automat pe server. Mărul în ramura comună și rularea testelor automate pe ea are loc după revizuirea reușită. Dacă testele din ramura comună au trecut, serviciul este actualizat automat în mediu de testare pe Amazon Elastic Container Service (stand). Standul este necesar tuturor dezvoltatorilor și testerilor, și distrugerea lui este nerecomandată. Testerii verifică pe acest mediu un fix sau o nouă caracteristică, efectuând teste manuale.
Arhitectura proiectului

Aplicația este formată din mai mult de zece servicii. Unele dintre ele sunt scrise în .NET Core, iar altele în NodeJs. Fiecare serviciu funcționează într-un container Docker în Amazon Elastic Container Service. Fiecare are propria bază de date Postgres, iar unele au și Redis. Nu există baze comune. Dacă mai multor servicii le sunt necesare aceleași date, aceste date sunt transmise fiecărui serviciu prin SNS (Simple Notification Service) și SQS (Amazon Simple Queue Service) în momentul modificării lor, iar serviciile le salvează în bazele lor separate.
SQS și SNS
SQS permite, prin protocolul HTTPS, să adauge mesaje în coadă și să citească mesaje din coadă.
Dacă mai multe servicii citesc aceeași coadă, fiecare mesaj ajunge doar la unul dintre ele. Acest lucru este util atunci când se lansează mai multe instanțe ale aceluiași serviciu pentru a distribui sarcina între ele.
Dacă este necesar ca fiecare mesaj să fie livrat mai multor servicii, fiecare destinat ar trebui să aibă propria coadă, iar pentru a duplica mesajele în mai multe cozi, este necesar SNS.
În SNS, creați un subiect și vă abonați la acesta, de exemplu, o coadă SQS. În subiect pot fi trimise mesaje. În acest fel, mesajul este trimis în fiecare coadă abonată la acest subiect. În SNS nu există o metodă pentru citirea mesajelor. Dacă, în procesul de depanare sau testare, este necesar să aflați ce se trimite în SNS, puteți crea o coadă SQS, să o abonați la subiectul dorit și să citiți coada.

API Gateway
Cele mai multe servicii nu sunt accesibile direct din internet. Accesul se realizează prin API Gateway, care verifică drepturile de acces. Acesta este și serviciul nostru, iar pentru el există și teste.
Notificări în timp real
Aplicația utilizează , pentru a arăta utilizatorului notificări în timp real. Aceasta este implementată în serviciul de notificări. El este accesibil direct din internet și funcționează singur cu OAuth, deoarece a fost considerat ineficient să se integreze suportul pentru WebSocket în Gateway, comparativ cu integrarea OAuth și a serviciului de notificări.
O abordare bine cunoscută pentru testare
Testele unitare înlocuiesc obiectele reale cu mock-uri pentru lucruri precum baza de date. Dacă un microserviciu, de exemplu, încearcă să creeze o înregistrare într-un tabel cu cheie externă, iar înregistrarea la care se referă acea cheie nu există, atunci cererea nu poate fi procesată. Testele unitare nu pot descoperi acest lucru.
În se recomandă utilizarea unei baze de date in-memory și injectarea de mock-uri.
Baza de date în memorie – este unul dintre sistemele de gestionare a bazelor de date (SGBD) acceptate de Entity Framework. A fost creată special pentru teste. Datele dintr-o astfel de bază sunt păstrate doar până la finalizarea procesului care o utilizează. Nu este necesară crearea de tabele și integritatea datelor nu este verificată.
Obiectele mock modelează clasă substitut doar atât cât înțelege dezvoltatorul testului cum funcționează aceasta.
Articolul de la Microsoft nu specifică cum să configurezi pornirea automată a Postgres și să efectuezi migrarea la pornirea testului. Soluția mea face acest lucru și, în plus, în microserviciu nu se adaugă cod special pentru teste.
Să trecem la soluție
În procesul de dezvoltare a devenit evident că testele unitare nu sunt suficiente pentru a identifica la timp toate problemele, așa că s-a decis să abordăm această problemă dintr-o altă perspectivă.
Configurarea mediului de testare
Prima sarcină este să desfășurăm mediul de testare. Pașii necesari pentru a lansa microserviciul sunt:
- Configurează serviciul testat pentru mediu local, specificând în variabilele de mediu detaliile de conectare la baza de date și AWS;
- Pornește Postgres și efectuează migrarea, rulând Liquibase.
În SGBD relaționale, înainte de a salva datele în bază, trebuie să creezi o schemă de date, pe scurt, tabele. La actualizarea aplicației, tabelele trebuie să fie aduse la forma utilizată de noua versiune, de preferat fără pierderi de date. Acest proces se numește migrare. Crearea tabelelor într-o bază inițial goală – este un caz particular de migrare. Migrarea poate fi încorporată în aplicație. Atât în .NET, cât și în NodeJS există cadre pentru migrare. În cazul nostru, din motive de securitate, microserviciile nu au dreptul de a modifica schema de date și migrarea se efectuează cu ajutorul Liquibase. - Pornește Amazon LocalStack. Este o implementare a serviciilor AWS pentru rulare local. Există o imagine gata pregătită în Docker Hub pentru LocalStack.
- Rulează scriptul pentru a crea în LocalStack entitățile necesare. Scripturile Shell folosesc AWS CLI.
Pentru testare în proiect este utilizat . A existat și înainte, dar era lansat manual și aplicația era testată după ce era deja desfășurată pe stand. Acest instrument permite efectuarea cererilor HTTP(S) arbitrare și verificarea conformității răspunsurilor cu așteptările. Cererile sunt grupate într-o colecție și se poate lansa întreaga colecție.

Cum este structurat testul automat
În timpul testului, totul funcționează în Docker: atât serviciul testat, cât și Postgres, instrumentul pentru migrație și Postman, sau mai bine zis, versiunea sa console – Newman.
Docker rezolvă o serie de probleme:
- Independența de configurația gazdei;
- Instalarea dependențelor: Docker descarcă imaginile de pe Docker Hub;
- Restaurarea sistemului la starea inițială: pur și simplu ștergem containerele.
Docker-compose îmbină containerele într-o rețea virtuală, izolată de internet, în care containerele se găsesc reciproc după numele de domeniu.
Testul este gestionat de un script shell. Pentru a rula testul pe Windows, folosim git-bash. Astfel, este suficient un singur script atât pentru Windows, cât și pentru Linux. Git și Docker sunt instalate la toți dezvoltatorii din proiect. La instalarea Git pe Windows, se instalează și git-bash, așa că toată lumea îl are.
Scriptul execută următorii pași:
- Construirea imaginilor Docker
docker-compose build - Pornirea DB și LocalStack
docker-compose up -d - Migrarea DB și pregătirea LocalStack
docker-compose run - Pornirea serviciului testat
docker-compose up -d - Pornirea testului (Newman)
- Oprirea tuturor containerelor
docker-compose down - Postarea rezultatelor în Slack
Avem un chat unde mesajele cu bifa verde sau crucea roșie și un link către log ajung.
În acești pași sunt folosite următoarele imagini Docker:
- Serviciul testat – aceeași imagine ca și pentru producție. Configurația pentru test – prin variabile de mediu.
- Pentru Postgres, Redis și LocalStack se folosesc imagini deja existente din Docker Hub. Pentru Liquibase și Newman au fost create de asemenea imagini. Le construim pe baza lor, adăugând fișierele noastre.
- Pentru pregătirea LocalStack se folosește o imagine existentă AWS CLI, și pe baza acesteia se creează o imagine care conține scriptul.
Folosind , nu este nevoie să construiești o imagine Docker doar pentru a adăuga fișiere în container. Totuși, volumes nu sunt potrivite pentru mediu nostru, deoarece sarcinile Gitlab CI funcționează în containere. Dintr-un astfel de container se poate controla Docker, dar volumes montează directoare doar din sistemul gazdă, nu din alt container.
Probleme cu care te poți confrunta
Așteptarea disponibilității
Când containerul cu serviciul este pornit, asta nu înseamnă că este gata să accepte conexiuni. Trebuie să aștepți conexiunea pentru a continua.
Această problemă este uneori rezolvată cu ajutorul unui script , care așteaptă oportunitatea de a stabili o conexiune TCP. Totuși, LocalStack poate genera o eroare 502 Bad Gateway. În plus, este alcătuit din multe servicii, iar dacă unul dintre ele este gata, acest lucru nu spune nimic despre celelalte.
Soluție: scripturile de pregătire a LocalStack, care așteaptă un răspuns 200 atât de la SQS, cât și de la SNS.
Conflicte între sarcini paralele
Mai multe teste pot rula simultan pe un singur host Docker, așa că numele containerelor și rețelelor trebuie să fie unice. În plus, testele din ramuri diferite ale aceleași servicii pot de asemenea rula simultan, astfel că nu este suficient să se precizeze numele lor în fiecare fișier compose.
Soluție: scriptul setează o valoare unică pentru variabila COMPOSE_PROJECT_NAME.
Particularități Windows
Când folosești Docker pe Windows, sunt câteva aspecte la care vreau să atrag atenția, deoarece această experiență este importantă pentru înțelegerea cauzelor erorilor.
- Scripturile shell din container trebuie să aibă terminatori de linie tip Linux.
Simbolul CR pentru shell este o eroare de sintaxă. Din mesajul de eroare, este dificil de înțeles că acesta este motivul. Când editezi astfel de scripturi în Windows, ai nevoie de un editor de text corect. În plus, sistemul de control al versiunilor trebuie configurat corect.
Iată cum se configurează git:
git config core.autocrlf input- Git-bash emulează folderele standard Linux și, atunci când este apelat un fișier exe (inclusiv docker.exe), înlocuiește căile absolute Linux cu căi Windows. Cu toate acestea, acest lucru nu are sens pentru căile care nu sunt pe mașina locală (sau căile din container). Această comportare nu poate fi dezactivată.
Soluție: adăuga un slash suplimentar la începutul căii: \/bin în loc de \/bin. Linux înțelege astfel de căi, pentru el mai multe slashes sunt echivalente cu unul singur. Dar git-bash nu recunoaște astfel de căi și nu încearcă să le transforme.
Ieșirea logurilor
Când se execută teste, mi-ar plăcea să văd logurile atât de la Newman, cât și de la serviciul testat. Deoarece evenimentele acestor loguri sunt legate între ele, combinarea lor într-o singură consolă este mult mai convenabilă decât două fișiere separate. Newman se lansează prin docker-compose run, iar, din această cauză, ieșirea sa ajunge în consolă. Rămâne să facem astfel încât și ieșirea serviciului să ajungă acolo.
Soluția inițială a constat în a face docker-compose up fără flag -d, dar, folosind capabilitățile shell-ului, să trimitem acest proces în fundal:
docker-compose up <service> &Aceasta a funcționat până când a fost necesar să trimitem logurile din Docker către un serviciu extern. docker-compose up a încetat să mai afișeze jurnalele în consola. Totuși, comanda a funcționat docker attach.
Soluție:
docker attach --no-stdin ${COMPOSE_PROJECT_NAME}__1 &Conflict de identificatori în iterațiile testului
Testele se desfășoară prin mai multe iterații. Baza de date nu este ștearsă în acest proces. Înregistrările din bază au ID-uri unice. Dacă scriem ID-uri specifice în interogări, în a doua iterație vom obține un conflict.
Pentru a evita acest lucru, fie ID-urile trebuie să fie unice, fie trebuie să ștergem toate obiectele create de test. Nu putem șterge anumite obiecte, conform cerințelor.
Soluție: generați GUID-uri cu scripturi în Postman.
var uuid = require('uuid');
var myid = uuid.v4();
pm.environment.set('myUUID', myid);Apoi, în interogare, folosiți simbolul {{myUUID}}, care va fi înlocuit cu valoarea variabilei.
Interacțiunea prin LocalStack
Dacă serviciul testat citește dintr-o coadă SQS sau scrie în ea, atunci pentru a verifica acest lucru, testul trebuie să funcționeze și cu această coadă.
Soluție: interogări din Postman către LocalStack.
API-urile serviciilor AWS sunt documentate, ceea ce permite realizarea de interogări fără SDK.
Dacă serviciul scrie în coadă, atunci o citim și verificăm conținutul mesajului.
Dacă serviciul trimite mesaje în SNS, în etapa de pregătire, LocalStack creează o coadă și se abonează la acest topic SNS. Totul se reduce la ceea ce am descris mai sus.
Dacă serviciul trebuie să citească un mesaj din coadă, atunci în pasul anterior al testului acest mesaj este scris în coadă.
Testarea interogărilor HTTP provenite de la microserviciul testat
Unele servicii funcționează prin HTTP cu ceva în afară de AWS, iar unele funcționalități AWS nu sunt implementate în LocalStack.
Soluție: în aceste cazuri poate ajuta , care are o imagine disponibilă în . Interogările și răspunsurile așteptate sunt configurate cu o interogare HTTP. API-ul este documentat, așa că facem interogări din Postman.
Testarea autentificării și autorizării OAuth
Folosim OAuth și . Pentru test avem nevoie de un furnizor OAuth pe care să-l putem rula local.
Toată interacțiunea serviciului cu furnizorul OAuth se compune din două interogări: mai întâi se solicită configurația /.well-known/openid-configuration, iar apoi se solicită cheia publică (JWKS) la adresa din configurație. Totul este conținut static.
Soluție: furnizorul nostru de testare OAuth – este un server de conținut static și două fișiere pe el. Tokenul a fost generat o singură dată și a fost comis în Git.
Particularitățile testării SignalR
Websocket-urile nu funcționează cu Postman. Un instrument special a fost creat pentru testarea SignalR.
Clientul SignalR nu poate fi doar un browser. Există o bibliotecă client pentru .NET Core. Clientul, scris în .NET Core, stabilește conexiunea, trece prin autentificare și așteaptă o anumită secvență de mesaje. Dacă primește un mesaj neașteptat sau conexiunea se întrerupe, clientul se încheie cu codul 1. La primirea ultimului mesaj așteptat, se încheie cu codul 0.
Newman funcționează simultan cu clientul. Se lansează mai mulți clienți pentru a verifica că mesajele sunt livrate tuturor celor care au nevoie.

Pentru a lansa mai mulți clienți, se folosește opțiunea —scale în linia de comandă docker-compose.
Înainte de a lansa Postman, scriptul așteaptă stabilirea conexiunii de către toți clienții.
Problema așteptării conexiunii ne-a mai întâlnit. Dar acolo erau servere, iar aici este client. Este nevoie de o altă abordare.
Soluție: clientul din container folosește mecanismul , pentru a comunica scriptului de pe host despre starea sa. Clientul creează un fișier pe un anumit traseu, să zicem, /healthcheck, imediat ce conexiunea este stabilită. Scriptul HealthCheck din Dockerfile arată astfel:
HEALTHCHECK --interval=3s CMD if [ ! -e /healthcheck ]; then false; fiComanda docker inspect arată pentru container statusul obișnuit, statusul de sănătate și codul de finalizare.
După finalizarea lui Newman, scriptul verifică că toate containerele clientului s-au încheiat, și anume, cu codul 0.
Fericirea există
După ce am depășit dificultățile descrise mai sus, am obținut un set de teste care funcționează stabil. În teste, fiecare serviciu funcționează ca un întreg, interacționează cu baza de date și cu Amazon LocalStack.
Aceste teste protejează echipa de 30+ dezvoltatori de erori în aplicație în contextul unei interacțiuni complexe între 10+ microservicii în timpul desfășurărilor frecvente.
Sursa: habr.com
