Come eseguire il black box test di un'app Go con RSpec

I test automatizzati sono di gran moda nello sviluppo web in questi giorni e continuano in tutto il settore. Un test ben scritto riduce drasticamente il rischio di interrompere accidentalmente un'applicazione quando si aggiungono nuove funzionalità o si correggono i bug. Quando hai un sistema complesso che è costruito da diversi componenti che interagiscono tra loro, è incredibilmente difficile testare come ciascun componente interagisce con altri componenti.

Diamo un'occhiata a come scrivere buoni test automatici per lo sviluppo di componenti in Go e come farlo utilizzando la libreria RSpec in Ruby on Rails.

Aggiunta Vai allo stack tecnologico del nostro progetto

Uno dei progetti a cui sto lavorando nella mia azienda, eTeam, può essere suddiviso in un pannello di amministrazione, dashboard utente, generatore di report e processore di richieste che gestisce le richieste di diversi servizi integrati nell'applicazione.

La parte del progetto che elabora le richieste è la più importante, quindi abbiamo dovuto massimizzarne l'affidabilità e la disponibilità.

Come parte di un'applicazione monolitica, esiste un alto rischio che un bug influisca sull'elaboratore della richiesta, anche quando ci sono cambiamenti nel codice in parti dell'app non correlate. Allo stesso modo, esiste il rischio di arresti anomali del processore delle richieste quando altri componenti sono sottoposti a carichi pesanti. Il numero di lavoratori Ngnix per l'app è limitato, il che può causare problemi all'aumentare del carico. Ad esempio, quando un certo numero di pagine ad alta intensità di risorse vengono aperte contemporaneamente nel pannello di amministrazione, il processore rallenta o addirittura arresta in modo anomalo l'intera app.

Questi rischi, nonché la maturità del sistema in questione - non abbiamo dovuto apportare importanti modifiche per mesi - hanno reso questa app un candidato ideale per la creazione di un servizio separato per la gestione delle richieste.

Abbiamo deciso di scrivere il servizio separato in Go, che ha condiviso l'accesso al database con l'applicazione Rails che è rimasta responsabile delle modifiche nella struttura della tabella. Con solo due applicazioni, un tale schema con un database condiviso funziona bene. Ecco come appariva:

Abbiamo scritto e distribuito il servizio in un'istanza Rails separata. In questo modo, non era necessario preoccuparsi che il processore delle richieste fosse interessato ogni volta che veniva distribuita l'app Rails. Il servizio accetta direttamente le richieste HTTP senza Ngnix e non utilizza molta memoria. Potresti chiamarla un'app minimalista!

Il problema con i test unitari in Go

Abbiamo creato unit test per l'applicazione Go in cui tutte le richieste del database sono state derise. Oltre ad altri argomenti per questa soluzione, l'applicazione principale di Rails era responsabile della struttura del database, quindi l'applicazione Go non disponeva effettivamente delle informazioni per la creazione di un database di prova. La metà dell'elaborazione è stata la logica aziendale, mentre l'altra metà erano le query del database, tutte derise.

Gli oggetti derisi sono molto meno leggibili in Go che in Ruby. Ogni volta che venivano aggiunte nuove funzioni per la lettura dei dati dal database, dovevamo aggiungere oggetti derisi durante molti test falliti che avevano funzionato in precedenza. Alla fine, tali test unitari non si sono dimostrati molto efficaci ed erano estremamente fragili.

La nostra soluzione

Per ovviare a questi inconvenienti, abbiamo deciso di coprire il servizio con test funzionali nell'applicazione Rails e testare il servizio in Go come una scatola nera. I test in white box non funzionerebbero in ogni caso, poiché era impossibile utilizzare Ruby per entrare nel servizio e vedere se veniva chiamato un metodo.

Ciò significa anche che le richieste inviate tramite il servizio di test erano impossibili da deridere, quindi avevamo bisogno di un'altra applicazione per gestire e scrivere questi test. Qualcosa come RequestBin avrebbe funzionato, ma doveva funzionare localmente. Avevamo già scritto un'utilità che avrebbe funzionato, quindi abbiamo deciso di provare a usarla.

Questa era la configurazione risultante:

  1. RSpec compila ed esegue il binario Go con la configurazione in cui viene specificato l'accesso al database di test insieme a una porta specifica per la ricezione di richieste HTTP, ovvero 8082.
  2. Esegue anche l'utilità, che registra le richieste HTTP che arrivano alla porta 8083.
  3. Scriviamo test regolari in RSpec. Questo crea i dati necessari nel database e invia una richiesta a localhost: 8082 come se fosse un servizio esterno come HTTParty.
  4. Analizziamo la risposta, controlliamo le modifiche nel database, riceviamo un elenco di richieste registrate dal sostituto RequestBin e le controlliamo.

Dettagli dell'attuazione

Ecco come lo abbiamo implementato. A titolo dimostrativo, chiamiamo il servizio di test TheService e creiamo un wrapper:

Vale la pena ricordare che i file di caricamento automatico devono essere configurati nella cartella di supporto quando si utilizza RSpec:

Dir [Rails.root.join ('spec / support / ** / *. Rb')]. Ogni {| f | richiedi f}

Il metodo di avvio:

  • Legge le informazioni di configurazione necessarie per avviare TheService. Queste informazioni possono differire tra i diversi sviluppatori e pertanto sono escluse da Git. La configurazione contiene le impostazioni necessarie per l'avvio del programma. Tutte queste diverse configurazioni si trovano in un'unica posizione, quindi non è necessario creare file non necessari.
  • Compila e corre attraverso go run
  • Esegue il polling ogni secondo e attende che TheService sia pronto ad accettare le richieste.
  • Registra l'identificatore di ogni processo al fine di non ripetere nulla e di avere la possibilità di interrompere un processo.

La configurazione stessa:

Il metodo "stop" interrompe semplicemente il processo. C'è un gotcha però! Ruby esegue un comando "go run", che compila TheService e avvia un file binario in un processo figlio con un ID sconosciuto. Se interrompiamo semplicemente il processo in esecuzione in Ruby, il processo figlio non si interrompe automaticamente e la porta rimarrà in uso. Pertanto, arrestare TheService deve passare attraverso l'ID del gruppo di processi:

Quindi prepariamo il "shared_context" in cui definiamo le variabili predefinite, avviamo TheService se non è già stato avviato e disattiviamo temporaneamente il videoregistratore poiché il videoregistratore vedrebbe quello che stiamo facendo come una richiesta di servizio esterna, ma non vogliamo Videoregistratore per deridere le richieste a questo punto:

E ora possiamo guardare a scrivere le specifiche stesse:

Il servizio può effettuare richieste HTTP a servizi esterni. Possiamo configurarlo per reindirizzare le richieste all'utilità locale che le registra. Per questa utility, esiste anche un wrapper per l'avvio e l'arresto simile a "TheServiceControl", tranne per il fatto che questa utility può essere avviata come binaria senza compilazione.

Punti salienti aggiuntivi

L'applicazione Go è stata scritta in modo che tutti i registri e le informazioni di debug vengano inviati a STDOUT. Durante la produzione, questo output viene inviato a un file. All'avvio da RSpec il registro viene visualizzato nella console, il che aiuta davvero con il debug.

Se esegui in modo specifico le specifiche che non necessitano di TheService, non inizierà.

Per non perdere tempo all'avvio di TheService ogni volta che cambia una specifica, durante il processo di sviluppo è possibile avviare TheService manualmente nel terminale e semplicemente non spegnerlo. Ogni volta che è necessario, puoi persino avviarlo in una modalità di debug IDE. Quindi le specifiche preparano tutto, inviano la richiesta al servizio, si interrompe e puoi facilmente eseguirne il debug. Questo rende l'approccio TDD davvero conveniente.

Conclusione

Usiamo questa configurazione da circa un anno e non abbiamo riscontrato alcun errore. Le specifiche sono molto più leggibili dei test unitari in Go e non si basano sulla conoscenza della struttura interna del servizio. Se, per qualche motivo, dobbiamo riscrivere il servizio in un'altra lingua, non dovremo modificare le specifiche. Solo i wrapper, utilizzati per l'avvio del servizio di test con un comando diverso, devono essere riscritti.