Test automatico dei microservizi in Docker per l'integrazione continua

Nei progetti legati allo sviluppo di architetture a microservizi, il CI/CD passa da un'opzione desiderabile a una necessità urgente. I test automatici sono una parte integrante dell'integrazione continua, un approccio intelligente al quale può regalare al team molte serate piacevoli con la famiglia e gli amici. In caso contrario, il progetto rischia di non essere mai completato.

È possibile coprire tutto il codice di un microservizio con unit test e oggetti mock, ma ciò risolve solo parzialmente il problema e lascia molte domande e complessità, soprattutto durante il test con i dati. Come sempre, i più critici sono: il test di coerenza dei dati in un DB relazionale, il test dell'integrazione con i servizi cloud e le errate assunzioni nella scrittura degli oggetti mock.

Tutto questo e un po' di più è risolvibile testando l'intero microservizio in un contenitore Docker. Un chiaro vantaggio per garantire la validità dei test è che gli stessi immagini Docker utilizzate in produzione vengono sottoposte a test.

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

  • conflitti tra attività parallele su un unico host Docker;
  • conflitti di identificatori nel DB durante le iterazioni del test;
  • attesa che i microservizi siano pronti;
  • aggregazione e invio dei log a sistemi esterni;
  • test delle richieste HTTP in uscita;
  • test dei websocket (utilizzando SignalR);
  • test dell'autenticazione e autorizzazione OAuth.

Questo è un articolo ispirato una mia presentazione al SECR 2019. Quindi per coloro che non hanno voglia di leggere, ecco la registrazione della presentazione.

Test automatico dei microservizi in Docker per l'integrazione continua

In questo articolo spiegherò come avviare in Docker il servizio da testare, il database e i servizi di Amazon AWS tramite uno script, eseguire i test su Postman e, dopo averli completati, fermare e rimuovere i contenitori creati. I test vengono eseguiti ad ogni modifica del codice. In questo modo, ci assicuriamo che ogni versione funzioni correttamente con il database e i servizi AWS.

Lo stesso script viene eseguito sia dagli sviluppatori sui propri desktop Windows, sia dal server Gitlab CI sotto Linux.

Affinché l'introduzione 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:

  • La rete non è mai completamente affidabile. Su mille richieste, una potrebbe non andare a buon fine;
    In questo caso, il test automatico non andrà a buon fine, il lavoro si fermerà e dovrà essere cercata la causa nei log;
  • Richieste troppo frequenti non sono ammesse da alcuni servizi esterni.

Inoltre, utilizzare un ambiente di test è sconsigliato, perché:

  • Non solo un codice errato in esecuzione può danneggiare l'ambiente, ma anche dati che il codice corretto non riesce a gestire;
  • Per quanto ci sforziamo di ripristinare tutte le modifiche effettuate dal test, durante il test stesso qualcosa potrebbe andare storto (altrimenti, perché eseguire il test?).

Informazioni sul progetto e sull'organizzazione del processo

La nostra azienda ha sviluppato un'applicazione web basata su microservizi, funzionante in Docker nel cloud Amazon AWS. Nel progetto venivano già utilizzati test unitari, ma spesso si verificavano errori che i test unitari non riuscivano a scoprire. Era necessario testare un intero microservizio insieme al database e ai servizi Amazon.

Nel progetto viene applicato 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. Il merge nel ramo principale e l'esecuzione dei test automatici su di esso avvengono dopo una revisione positiva. Se i test nel ramo principale hanno successo, il servizio viene aggiornato automaticamente nell'ambiente di test su Amazon Elastic Container Service (ambiente di test). L'ambiente è necessario per tutti gli sviluppatori e tester, ed è sconsigliato danneggiarlo. I tester verificano su questo ambiente la correzione o la nuova funzionalità, effettuando test manuali.

Architettura del progetto

Test automatico dei microservizi in Docker per l'integrazione continua

L'applicazione consiste in più di dieci servizi. Alcuni di essi sono scritti in .NET Core, mentre altri in NodeJs. Ogni servizio funziona in un container Docker in Amazon Elastic Container Service. Ognuno ha il proprio database Postgres e alcuni hanno anche Redis. Non ci sono database condivisi. Se più servizi hanno bisogno degli stessi dati, questi vengono trasmessi a ciascuno di essi attraverso SNS (Simple Notification Service) e SQS (Amazon Simple Queue Service) nel momento in cui vengono modificati, e i servizi li salvano nei loro database separati.

SQS e SNS

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

Se più servizi leggono una stessa coda, ogni messaggio viene ricevuto solo da uno di essi. Questo è utile quando si eseguono più istanze dello stesso servizio per distribuire il carico tra di esse.

Se è necessario che ogni messaggio venga recapitato a più servizi, ogni destinatario deve avere la propria coda e per il duplicato dei messaggi in più code è necessario SNS.

In SNS si crea un topic e ci si iscrive, ad esempio, a una coda SQS. È possibile inviare messaggi al topic. In questo modo, il messaggio viene inviato a ogni coda iscritta a questo topic. In SNS non c'è un metodo per leggere i messaggi. Se durante il debug o il test è necessario sapere cosa viene inviato a SNS, è possibile creare una coda SQS, iscriverla al topic desiderato e leggere la coda.

Test automatico 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. Questo è anche il nostro servizio, e ci sono test anche per esso.

Notifiche in tempo reale

L'applicazione utilizza SignalR, per mostrare all'utente notifiche in tempo reale. Questo è implementato nel servizio di notifiche. È accessibile direttamente da Internet e funziona autonomamente con OAuth, perché è risultato poco pratico integrare il supporto per WebSocket nel Gateway rispetto all'integrazione con OAuth e il servizio di notifiche.

Un approccio noto ai test

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

In articolo di Microsoft si propone di utilizzare un database in memoria e di iniettare oggetti fittizi.

Il database in memoria è uno dei database supportati da Entity Framework. È stato creato appositamente per i test. I dati in un tale database vengono memorizzati solo fino al completamento del processo che lo utilizza. Non è necessario creare tabelle e l'integrità dei dati non viene verificata.

Gli oggetti mock simulano una classe sostituibile solo nella misura in cui lo sviluppatore del test ne comprende il funzionamento.

Nell'articolo di Microsoft non è indicato come avviare automaticamente Postgres e eseguire la migrazione all'avvio del test. La mia soluzione lo fa e, inoltre, non viene aggiunto alcun codice specifico per i test nel microservizio.

Passiamo alla soluzione

Durante lo sviluppo è diventato chiaro che i test unitari non sono 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 è implementare l'ambiente di test. I passaggi necessari per avviare il microservizio sono:

  • Configurare il servizio da testare per l'ambiente locale, specificando nelle variabili d'ambiente le credenziali per la connessione al database e ad AWS;
  • Avviare Postgres e eseguire la migrazione avviando Liquibase.
    Nei database relazionali, prima di registrare i dati nel database, è necessario creare uno schema dei dati, in altre parole, tabelle. Quando si aggiorna l'applicazione, le tabelle devono essere adattate al formato utilizzato dalla nuova versione, possibilmente 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 incorporata nell'applicazione stessa. In .NET e in NodeJS ci sono 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 eseguita tramite Liquibase.
  • Avviare Amazon LocalStack. Questa è l'implementazione dei servizi AWS per l'esecuzione in locale. Per LocalStack è disponibile un'immagine pronta su Docker Hub.
  • Eseguire lo script per creare in LocalStack le entità necessarie. Gli script Shell utilizzano AWS CLI.

Per i test nel progetto viene utilizzato Postman. Era presente anche prima, ma veniva avviato manualmente e testava l'applicazione già implementata sulla piattaforma. Questo strumento consente di effettuare richieste HTTP(S) arbitrarie e verificare la corrispondenza delle risposte con le aspettative. Le richieste vengono raggruppate in una collezione e si può avviare l'intera collezione.

Test automatico dei microservizi in Docker per l'integrazione continua

Come è strutturato il test automatico

Durante il test, tutto funziona in Docker: sia il servizio in fase di test, sia Postgres, sia lo strumento di migrazione, e Postman, o meglio, la sua versione 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 all'altro per nome 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. Quando si installa Git su Windows, viene installato git-bash, quindi ce l'hanno tutti.

Lo script esegue i seguenti passaggi:

  • Costruzione delle immagini Docker
    docker-compose build
  • Avvio del DB e di LocalStack
    docker-compose up -d
  • Migrazione del DB e preparazione di LocalStack
    docker-compose run
  • Avvio del servizio in fase di test
    docker-compose up -d
  • Esecuzione del test (Newman)
  • Arresto di tutti i contenitori
    docker-compose down
  • Pubblicazione dei risultati su Slack
    Abbiamo una chat in cui arrivano messaggi con un segno di spunta verde o una croce rossa e un link al log.

In questi passaggi sono coinvolte le seguenti immagini Docker:

  • Il servizio in fase di test utilizza la stessa immagine di quella in produzione. La configurazione per il test è tramite variabili ambiente.
  • Per Postgres, Redis e LocalStack vengono utilizzate immagini pronte da Docker Hub. Anche per Liquibase e Newman ci sono immagini pronte. Costruiremo le nostre sulla loro base, aggiungendo i nostri file.
  • Per preparare LocalStack viene utilizzata un'immagine predefinita di AWS CLI, sulla quale 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 per il nostro ambiente, perché i compiti di Gitlab CI funzionano già nei contenitori. Da un 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 della disponibilità

Quando il contenitore con il servizio è avviato, non significa ancora che sia pronto per accettare connessioni. Occorre attendere la connessione per continuare.

A volte questo problema viene risolto con uno script wait-for-it.sh, che attende la possibilità di stabilire una connessione TCP. Tuttavia, LocalStack può restituire un errore 502 Bad Gateway. Inoltre, è composto da molti servizi e, se uno di essi è pronto, ciò non dice nulla sugli altri.

Soluzione: gli script di preparazione di LocalStack, che attendono una risposta 200 sia da SQS che da SNS.

Conflitti di task paralleli

Più test possono funzionare contemporaneamente su un singolo host Docker, quindi i nomi dei contenitori e delle reti devono essere unici. Inoltre, i test di rami diversi di un servizio possono funzionare simultaneamente, quindi non è sufficiente specificare i propri nomi in ogni file compose.

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

Caratteristiche di Windows

Quando si utilizza Docker in Windows, ci sono diverse cose a cui voglio richiamare la vostra attenzione, poiché questa esperienza è importante per comprendere le cause degli errori.

  1. Gli shell script nel contenitore devono avere fine linea in stile Linux.
    Il simbolo CR per la shell è un errore sintattico. Dalla segnalazione dell'errore è difficile capire che il problema è questo. Quando si modificano tali script in Windows è necessario un editor di testo appropriato. Inoltre, il sistema di controllo versione deve essere configurato correttamente.

Ecco come si imposta git:

git config core.autocrlf input

  1. Git-bash emula le directory standard di Linux e, quando viene chiamato un file exe (incluso docker.exe), sostituisce i percorsi Linux assoluti con percorsi Windows. Tuttavia, ciò non ha senso per i percorsi che non si trovano sulla macchina locale (o per i percorsi nel contenitore). Comportamento di questo tipo non può essere disabilitato.

Soluzione: aggiungere una barra aggiuntiva all'inizio del percorso: \/\/bin invece di \/bin. Linux comprende questi percorsi, per lui più barre significano la stessa cosa di una. Ma git-bash non riconosce tali 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 da testare. Poiché gli eventi di questi log sono correlati tra loro, combinarli in una sola console è molto più comodo rispetto a due file separati. Newman viene avviato tramite docker-compose run, e quindi il suo output finisce nella console. Resta da fare in modo che anche l'output del servizio venga mostrato lì.

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 ha funzionato finché non è stato necessario inviare i log da Docker a un servizio esterno. docker-compose up ha smesso di visualizzare i log nella console. Tuttavia, il comando funzionava docker attach.

Soluzione:

docker attach --no-stdin ${COMPOSE_PROJECT_NAME}__1 &

Conflitto degli identificatori durante le iterazioni del test

I test vengono eseguiti mediante più iterazioni. Il database non viene svuotato. Le registrazioni nel database hanno ID unici. Se scriviamo ID specifici nelle richieste, nella seconda iterazione avremo conflitti.

Per evitarlo, gli ID devono essere unici oppure è necessario eliminare tutti gli oggetti creati dal test. Alcuni oggetti non possono essere eliminati, secondo i requisiti.

Soluzione: generare GUID tramite script in Postman.

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

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

Interazione tramite LocalStack

Se il servizio testato legge una coda SQS o ci scrive, allora anche il test deve lavorare con questa coda per verificarne il funzionamento.

Soluzione: richieste da Postman a LocalStack.

L'API dei servizi AWS è documentata, permettendo di effettuare richieste senza SDK.

Se un servizio scrive in una coda, la leggiamo e controlliamo il contenuto del messaggio.

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

Se un servizio deve leggere un messaggio da una 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 funzioni AWS non sono implementate in LocalStack.

Soluzione: in questi casi può aiutare MockServer, che ha già un'immagine pronta in Docker Hub. Le richieste e le risposte attese vengono configurate con una richiesta HTTP. L'API è documentata, quindi facciamo richieste da Postman.

Test dell'autenticazione e autorizzazione OAuth

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

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

Soluzione: il nostro provider OAuth di test è un server di contenuti statici e due file su di esso. Il token è stato generato una sola volta e inserito nel Git.

Caratteristiche del testing di SignalR

Con i web socket Postman non funziona. È stato creato uno strumento speciale per testare SignalR.

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

Contemporaneamente al client, funziona Newman. Vengono avviati più client per verificare che i messaggi vengano consegnati a tutti coloro che ne hanno bisogno.

Test automatico 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 la connessione.
Il problema dell'attesa della connessione ci è già capitato. Ma lì c'erano server, qui c'è un client. Serve un approccio diverso.

Soluzione: il client nel contenitore utilizza il meccanismo HealthCheck, per comunicare allo script sull'host il proprio stato. Il client crea un file a 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 contenitore lo stato normale, lo stato di salute e il codice di uscita.

Dopo il completamento di Newman, lo script controlla che tutti i contenitori con il client siano terminati, e precisamente, con codice 0.

La felicità esiste

Dopo aver superato le difficoltà sopra descritte, abbiamo un insieme di test che funzionano in modo stabile. Negli 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 oltre 10 microservizi durante i 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