Test automatici dei microservizi in Docker per l'integrazione continua

Nella progettazione di architetture a microservizi, il CI/CD da una comoda opzione diventa una necessità urgente. I test automatici sono parte integrante dell'integrazione continua, e un approccio ben pianificato può regalare al team molte serate piacevoli con la famiglia e gli amici. Altrimenti, il progetto rischia di non essere mai completato.

È possibile coprire l'intero codice di un microservizio con test unitari utilizzando oggetti fittizi, ma questo risolve solo parzialmente il problema e lascia molte domande e complicazioni, specialmente durante il test delle operazioni con i dati. Come sempre, le questioni più critiche riguardano il test della coerenza dei dati in un RDB, il test delle interazioni con i servizi cloud e le ipotesi errate durante la scrittura degli oggetti fittizi.

Tutto questo e molto di più viene affrontato testando un microservizio completo all'interno di un container Docker. Un indiscusso vantaggio per garantire la validità dei test è che vengono utilizzate le stesse immagini Docker che saranno messe in produzione.

L'automazione di questo approccio presenta una serie di problemi, la cui soluzione verrà descritta più avanti:

  • conflitti tra task paralleli su un singolo host Docker;
  • conflitti di identificatori nel DB durante le iterazioni del test;
  • attesa della disponibilità dei microservizi;
  • unione e invio dei log a sistemi esterni;
  • test di richieste HTTP in uscita;
  • test di websocket (utilizzando SignalR);
  • test di autenticazione e autorizzazione OAuth.

Questo articolo è ispirato da sul mio intervento SECR 2019. Quindi per chi ha poca voglia di leggere, ecco la registrazione della presentazione..

Test automatici dei microservizi in Docker per l'integrazione continua

In questo articolo spiegherò come utilizzare uno script per avviare in Docker il servizio da testare, il database e i servizi Amazon AWS, eseguire test su Postman e, dopo il loro completamento, fermare ed eliminare i container creati. I test vengono eseguiti ad ogni modifica del codice. In questo modo, ci assicuriamo che ciascuna versione funzioni correttamente con il database e i servizi AWS.

Lo stesso script viene eseguito sia dagli sviluppatori sui loro desktop Windows, sia dal server GitLab CI su Linux.

Affinché l'implementazione di nuovi test sia giustificata, non deve richiedere l'installazione di strumenti aggiuntivi né sul computer dello sviluppatore, né sul server dove i test vengono eseguiti al momento del commit. Docker risolve questo problema.

Il test deve funzionare su un server locale per i seguenti motivi:

  • Le reti non sono mai completamente affidabili. Su mille richieste, una potrebbe non andare a buon fine;
    In tal caso, il test automatico non andrà a buon fine, il lavoro si fermerà e sarà necessario cercare la causa nei log;
  • Richieste troppo frequenti non sono consentite da alcuni servizi esterni.

Inoltre, utilizzare un ambiente di test non è consigliabile per i seguenti motivi:

  • Un ambiente di test può essere compromesso non solo da un codice difettoso che vi gira, ma anche da dati che un codice corretto non può elaborare;
  • Per quanto ci sforziamo di ripristinare tutte le modifiche apportate dal test durante il suo svolgimento, qualcosa potrebbe andare storto (altrimenti, perché testarlo?).

Informazioni sul progetto e sull'organizzazione del processo

La nostra azienda ha sviluppato un'applicazione web basata su microservizi, operante in Docker su Amazon AWS. Nel progetto erano già stati utilizzati test unitari, ma frequentemente si verificavano errori che i test unitari non riuscivano a rilevare. Era necessario testare l'intero microservizio insieme al database e ai servizi di Amazon.

Nel progetto viene utilizzato un processo standard di integrazione continua, che include il test del microservizio ad ogni commit. Dopo l'assegnazione del compito, lo sviluppatore apporta modifiche al microservizio, lo testa manualmente e avvia tutti i test automatici disponibili. Se necessario, lo sviluppatore modifica i test. Se non vengono riscontrati problemi, viene effettuato un commit nel ramo di questo compito. Dopo ogni commit, i test vengono avviati automaticamente sul server. La fusione nel ramo principale e l'esecuzione dei test automatici su di esso avvengono dopo una revisione positiva. Se i test nel ramo principale hanno esito positivo, il servizio si aggiorna automaticamente nell'ambiente di test su Amazon Elastic Container Service (stand). Lo stand è necessario per tutti gli sviluppatori e tester ed è sconsigliabile romperlo. I tester in questo ambiente verificano la correzione o la nuova funzionalità eseguendo test manuali.

Architettura del progetto

Test automatici dei microservizi in Docker per l'integrazione continua

L'applicazione è composta da più di dieci servizi. Alcuni di essi sono scritti in .NET Core e altri in NodeJs. Ogni servizio funziona all'interno di un contenitore Docker nel servizio Amazon Elastic Container Service. Ognuno ha il proprio database Postgres, e alcuni hanno anche Redis. Non ci sono database condivisi. Se più servizi necessitano degli stessi dati, questi vengono trasferiti a ciascuno di essi tramite SNS (Simple Notification Service) e SQS (Amazon Simple Queue Service) al momento della loro modifica, e i servizi li salvano nei propri database isolati.

SQS e SNS

SQS consente di inviare messaggi in coda e leggerli dalla coda tramite protocollo HTTPS.

Se più servizi leggono da una stessa coda, ogni messaggio arriva solo a uno di essi. Questo è utile quando si avviano più istanze di un servizio per distribuire il carico tra di esse.

Se è necessario che ogni messaggio sia consegnato a più servizi, ogni destinatario deve avere la propria coda, e per duplicare i messaggi in più code è necessario SNS.

In SNS, crei un topic e ti iscrivi ad esso, ad esempio, una coda SQS. È possibile inviare messaggi al topic. In questo modo, il messaggio viene inviato a ogni coda iscritta a quel topic. In SNS non esiste un metodo per leggere i messaggi. Se durante il debug o i test è necessario sapere cosa viene inviato a SNS, puoi creare una coda SQS, iscriverla al topic desiderato e leggere la coda.

Test automatici dei microservizi in Docker per l'integrazione continua

API Gateway

La maggior parte dei servizi non è accessibile direttamente da Internet. L'accesso avviene tramite API Gateway, che verifica i diritti di accesso. Anche questo è un nostro servizio, e ci sono anche test per esso.

Notifiche in tempo reale

L'applicazione utilizza SignalR, per mostrare agli utenti notifiche in tempo reale. Questo è implementato nel servizio notifiche. È accessibile direttamente da Internet e gestisce autonomamente OAuth, poiché integrarlo con il Gateway per supportare i WebSocket si è rivelato poco pratico rispetto all'integrazione di OAuth e del servizio notifiche.

Approccio noto ai test

I test unitari sostituiscono oggetti fittizi come il database. Se un microservizio, ad esempio, cerca di creare un record in una tabella con una chiave esterna, e il record a cui questa chiave fa riferimento non esiste, la richiesta non può essere elaborata. I test unitari non possono rilevarlo.

In articolo di Microsoft si consiglia di utilizzare un database in-memory e di implementare oggetti fittizi.

Il database in-memory è uno dei DBMS supportati da Entity Framework. È stato creato appositamente per i test. I dati in un database di questo tipo vengono conservati solo fino al termine del processo che lo utilizza. Non è necessario creare tabelle e l'integrità dei dati non viene controllata.

Gli oggetti fittizi modellano la classe sostituita solo nella misura in cui lo sviluppatore del test comprende il suo funzionamento.

Nell'articolo di Microsoft non viene specificato come ottenere l'avvio automatico di Postgres e l'esecuzione della migrazione all'avvio del test. La mia soluzione lo fa e, inoltre, non aggiunge alcun codice specifico per i test nel microservizio stesso.

Passiamo alla soluzione

Durante lo sviluppo è emerso che i test unitari non erano sufficienti per individuare tempestivamente tutti i problemi, quindi è stato deciso di affrontare la questione da un'altra prospettiva.

Configurazione dell'ambiente di test

Il primo compito consiste nel configurare l'ambiente di test. I passaggi necessari per avviare il microservizio sono:

  • Configurare il servizio da testare nell'ambiente locale, specificando nelle variabili d'ambiente i dettagli per la connessione al database e ad AWS;
  • Avviare Postgres ed eseguire la migrazione utilizzando Liquibase.
    Nelle basi di dati relazionali, prima di registrare i dati nel database, è necessario creare lo schema dei dati, in altre parole, le tabelle. Quando l'applicazione viene aggiornata, le tabelle devono essere adattate allo schema utilizzato dalla nuova versione, preferibilmente senza perdita di dati. Questo si chiama migrazione. La creazione di tabelle in un database inizialmente vuoto è un caso particolare di migrazione. La migrazione può essere integrata nell'applicazione stessa. Sia .NET che NodeJS dispongono di framework per la migrazione. Nel nostro caso, per motivi di sicurezza, i microservizi non hanno il diritto di modificare lo schema dei dati, e la migrazione viene effettuata tramite Liquibase.
  • Avvia Amazon LocalStack. Questa è un'implementazione dei servizi AWS per l'esecuzione in locale. Esiste un'immagine pronta di LocalStack su Docker Hub.
  • Esegui lo script per creare nel LocalStack le entità necessarie. Gli script Shell utilizzano AWS CLI.

Per il collaudo del progetto si utilizza Postman. Era già presente in passato, ma veniva eseguito manualmente per testare l'applicazione già distribuita sull'ambiente. Questo strumento consente di effettuare richieste HTTP(S) arbitrarie e verificare che le risposte corrispondano alle aspettative. Le richieste vengono raggruppate in una collezione e si può eseguire l'intera collezione.

Test automatici dei microservizi in Docker per l'integrazione continua

Come è strutturato il test automatico

Durante il test, tutto funziona in Docker: sia il servizio testato, sia Postgres, sia lo strumento di migrazione, e Postman, o meglio, la sua versione da console – Newman.

Docker risolve una serie di problemi:

  • Indipendenza dalla configurazione dell'host;
  • Installazione delle dipendenze: Docker scarica le immagini da Docker Hub;
  • Ripristino del sistema allo stato originale: basta rimuovere i contenitori.

Docker-compose unisce i contenitori in una rete virtuale, isolata da Internet, nella quale i contenitori si trovano l'uno con l'altro tramite nomi di dominio.

Il test è gestito da uno script shell. Per eseguire il test su Windows utilizziamo git-bash. In questo modo, è sufficiente uno script sia per Windows che per Linux. Git e Docker sono installati per tutti gli sviluppatori del progetto. Durante l'installazione di Git su Windows, viene installato anche git-bash, quindi ce l'hanno tutti.

Lo script esegue i seguenti passaggi:

  • Costruzione delle immagini Docker
    docker-compose build
  • Avvio del database e di LocalStack
    docker-compose up -d
  • Migrazione del database e preparazione di LocalStack
    docker-compose run
  • Avvio del servizio da testare
    docker-compose up -d
  • Avvio del test (Newman)
  • Arresto di tutti i contenitori
    docker-compose down
  • Pubblicazione dei risultati su Slack
    Abbiamo una chat dove vengono inviati messaggi con il segno di spunta verde o la croce rossa e un link al log.

In questi passaggi vengono utilizzate le seguenti immagini Docker:

  • Il servizio da testare è la stessa immagine utilizzata in produzione. La configurazione per il test avviene tramite variabili d'ambiente.
  • Per Postgres, Redis e LocalStack utilizziamo immagini pronti da Docker Hub. Ci sono anche immagini pronte per Liquibase e Newman. Costruiamo le nostre sulla loro base, aggiungendo i nostri file.
  • Per la preparazione di LocalStack utilizziamo un'immagine pronta di AWS CLI, e su quella base viene creata un'immagine contenente lo script.

Utilizzando volumi, non è necessario costruire un'immagine Docker solo per aggiungere file al contenitore. Tuttavia, i volumi non sono adatti al nostro ambiente, perché i compiti di GitLab CI vengono eseguiti all'interno dei contenitori. Da tale contenitore è possibile gestire Docker, ma i volumi montano solo cartelle dal sistema host, non da un altro contenitore.

Problemi che si possono incontrare

Attesa per la disponibilità

Quando il contenitore con il servizio è avviato, non significa ancora che sia pronto a ricevere connessioni. Bisogna aspettare una connessione per poter continuare.

Questa questione a volte viene risolta tramite uno script wait-for-it.sh, che attende la possibilità di stabilire una connessione TCP. Tuttavia, LocalStack potrebbe restituire un errore 502 Bad Gateway. Inoltre, è composto da molti servizi e, se uno di essi è pronto, questo non dice nulla sugli altri.

Soluzione: script di preparazione di LocalStack che aspettano una risposta 200 sia da SQS che da SNS.

Conflitti di compiti paralleli

È possibile eseguire più test contemporaneamente su un singolo host Docker, quindi i nomi dei contenitori e delle reti devono essere unici. Inoltre, i test provenienti da rami diversi di un servizio possono anch'essi essere eseguiti contemporaneamente, pertanto non basta specificare nomi diversi in ciascun file compose.

Soluzione: lo script imposta un valore unico per la variabile COMPOSE_PROJECT_NAME.

Caratteristiche di Windows

Quando si utilizza Docker su Windows, ci sono alcune cose che desidero sottolineare, poiché questa esperienza è fondamentale per comprendere le cause degli errori.

  1. Gli script shell all'interno del contenitore devono avere terminazioni di riga in formato Linux.
    Il simbolo CR nello shell è un errore di sintassi. Dalla segnalazione dell'errore, è difficile capire che il problema sia questo. Quando si modificano tali script su Windows, è necessario utilizzare un editor di testo adeguato. Inoltre, il sistema di controllo versioni deve essere configurato correttamente.

Ecco come configurare git:

git config core.autocrlf input

  1. Git-bash emula le cartelle standard di Linux e, quando viene chiamato un file exe (incluso docker.exe), sostituisce i percorsi assoluti di Linux con percorsi di Windows. Tuttavia, questo non ha senso per i percorsi non sulla macchina locale (o per i percorsi nel contenitore). Questo comportamento non può essere disattivato.

Soluzione: aggiungere uno slash aggiuntivo all'inizio del percorso: //bin invece di /bin. Linux comprende questi percorsi, per lui più di uno slash equivale a uno. Tuttavia, git-bash non riconosce questi percorsi e non cerca di convertirli.

Output dei log

Durante l'esecuzione dei test, sarebbe utile vedere i log sia di Newman che del servizio testato. Poiché gli eventi di questi log sono correlati, combinarli in un'unica console è molto più comodo rispetto a due file separati. Newman viene eseguito tramite docker-compose run, e quindi il suo output finisce nella console. Resta da fare in modo che anche l'output del servizio venga incluso.

La soluzione iniziale consisteva nel fare docker-compose up senza il flag -d, ma utilizzando le capacità della shell, inviare questo processo in background:

docker-compose up <service> &

Questo funzionava fino a quando non è stato necessario inviare i log da docker a un servizio esterno. docker-compose up ha smesso di mostrare i log nella console. Tuttavia, il comando docker attach.

Soluzione:

docker attach --no-stdin ${COMPOSE_PROJECT_NAME}_<service>_1 &

Conflitto degli identificatori durante le iterazioni del test

I test vengono eseguiti in più iterazioni. Il database non viene pulito. Le registrazioni nel database hanno ID univoci. Se si registrano ID specifici nelle richieste, nella seconda iterazione si verificherà un conflitto.

Per evitare ciò, gli ID devono essere univoci oppure è necessario eliminare tutti gli oggetti creati dal test. Alcuni oggetti non possono essere eliminati, in conformità ai requisiti.

Soluzione: generare GUID con script in Postman.

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

Poi, nella richiesta utilizzare il simbolo {{myUUID}}, che verrà sostituito con il valore della variabile.

Interazione tramite LocalStack

Se il servizio testato legge dalla coda SQS o vi scrive, il test stesso deve lavorare anche con questa coda per poter effettuare il controllo.

Soluzione: richieste da Postman a LocalStack.

Le API dei servizi AWS sono documentate, il che permette di effettuare richieste senza SDK.

Se il servizio scrive nella coda, la leggiamo e controlliamo il contenuto del messaggio.

Se il servizio invia messaggi a SNS, durante la preparazione, LocalStack crea anche una coda e si iscrive a questo argomento SNS. Dopodiché, il tutto si riduce a quanto descritto sopra.

Se il servizio deve leggere un messaggio dalla coda, nel passaggio precedente del test scriviamo questo messaggio nella coda.

Test delle richieste HTTP in uscita dal microservizio testato

Alcuni servizi funzionano tramite HTTP con qualcosa di diverso da AWS, e alcune funzionalità di AWS non sono implementate in LocalStack.

Soluzione: in questi casi può essere utile MockServer, che ha un'immagine pronta in Docker Hub. Le richieste e le risposte attese sono configurate tramite una richiesta HTTP. L'API è documentata, quindi possiamo effettuare richieste da Postman.

Test dell'autenticazione e autorizzazione OAuth

Utilizziamo OAuth e JSON Web Tokens (JWT). Per il test è necessario un fornitore OAuth che possiamo eseguire localmente.

Tutta l'interazione del servizio con il fornitore OAuth si riduce a due richieste: prima richiediamo la configurazione /.well-known/openid-configuration, e poi richiediamo la chiave pubblica (JWKS) all'indirizzo fornito nella configurazione. Tutto questo è contenuto statico.

Soluzione: il nostro fornitore OAuth di test è un server di contenuto statico con due file su di esso. Il token è stato generato una sola volta e committato in Git.

Caratteristiche del testing di SignalR

Postman non funziona con i websocket. È stato creato uno strumento speciale per il test di SignalR.

Il client SignalR non è limitato solo ai browser. Esiste una libreria client per .NET Core. Un client scritto in .NET Core stabilisce una connessione, passa attraverso l'autenticazione e attende una sequenza specifica di messaggi. Se riceve un messaggio inaspettato o la connessione si interrompe, il client termina con codice 1. Alla ricezione dell'ultimo messaggio atteso, si chiude con codice 0.

Contemporaneamente al client, funziona Newman. Vengono avviati più client per verificare che i messaggi vengano consegnati a tutti quelli che devono riceverli.

Test automatici dei microservizi in Docker per l'integrazione continua

Per avviare più client, si utilizza l'opzione —scale nella riga di comando di docker-compose.

Prima di avviare Postman, lo script attende che tutti i client stabiliscano una connessione.
Il problema dell'attesa della connessione lo abbiamo già incontrato. Ma lì si trattava di server, qui di un client. Serve un approccio diverso.

Soluzione: il client nel contenitore utilizza il meccanismo HealthCheck, per informare lo script sull'host del proprio stato. Il client crea un file in un percorso specifico, ad esempio, /healthcheck, non appena la connessione è stabilita. Lo script HealthCheck nel Dockerfile appare così:

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

Team docker inspect mostra per il container lo stato normale, lo stato di salute e il codice di uscita.

Dopo la conclusione di Newman, lo script verifica che tutti i container del cliente siano terminati e con codice 0.

La felicità esiste

Dopo aver superato le difficoltà descritte sopra, abbiamo un insieme di test che funzionano in modo stabile. Nei test, ogni servizio opera come un'unità, interagendo con il database e con Amazon LocalStack.

Questi test proteggono un team di oltre 30 sviluppatori da errori nell'applicazione con interazioni complesse tra 10+ microservizi durante frequenti deploy.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster