Test di codice intensivo dei dati con Go, parte 5

Panoramica

Questa è la parte cinque su cinque in una serie di tutorial sul test del codice ad alta intensità di dati. Nella parte quattro, ho trattato gli archivi di dati remoti, utilizzando database di test condivisi, utilizzando le istantanee dei dati di produzione e generando i propri dati di test. In questo tutorial, cercherò di testare i fuzz, testare la cache, testare l'integrità dei dati, testare l'idempotenza e dati mancanti.

Fuzz Testing

L'idea di test fuzz è di sopraffare il sistema con un sacco di input casuali. Invece di cercare di pensare a un input che copra tutti i casi, che può essere difficile e / o molto laborioso, lasci che sia il caso a farlo per te. È concettualmente simile alla generazione casuale di dati, ma l'intenzione qui è di generare input casuali o semi-casuali piuttosto che dati persistenti.

Quando è utile il test Fuzz?

Il test fuzz è utile in particolare per la ricerca di problemi di sicurezza e di prestazioni quando input inaspettati causano arresti anomali o perdite di memoria. Ma può anche aiutare a garantire che tutti gli input non validi vengano rilevati in anticipo e vengano rifiutati correttamente dal sistema.

Consideriamo, per esempio, l'input che si presenta sotto forma di documenti JSON profondamente annidati (molto comune nelle web API). Cercando di generare manualmente un elenco completo di casi di test è sia soggetto a errori che molto lavoro. Ma il test fuzz è la tecnica perfetta.

Utilizzando Fuzz Testing 

Ci sono diverse librerie che puoi usare per il test fuzz. Il mio preferito è gofuzz ​​di Google. Ecco un semplice esempio che genera automaticamente 200 oggetti unici di una struttura con diversi campi, inclusa una struttura annidata.  

import ("fmt" "github.com/google/gofuzz") func SimpleFuzzing () type SomeType struct A stringa B stringa C int D struct E float64 f: = fuzz.New () oggetto: = SomeType   uniqueObjects: = map [SomeType] int  per i: = 0; io < 200; i++  f.Fuzz(&object) uniqueObjects[object]++  fmt.Printf("Got %v unique objects.\n", len(uniqueObjects)) // Output: // Got 200 unique objects.  

Test della tua cache

Praticamente ogni sistema complesso che si occupa di molti dati ha una cache, o più probabilmente diversi livelli di cache gerarchiche. Come dice il proverbio, ci sono solo due cose difficili in informatica: nominare le cose, invalidare la cache e disattivare un errore.

Scherzi a parte, la gestione della strategia di caching e l'implementazione possono complicare l'accesso ai dati, ma hanno un enorme impatto sui costi e sulle prestazioni di accesso ai dati. Il test della cache non può essere eseguito dall'esterno perché l'interfaccia nasconde da dove provengono i dati e il meccanismo della cache è un dettaglio di implementazione.

Vediamo come testare il comportamento della cache del livello dati ibrido di Songify.

Cache Hits and Misses

Le cache vivono e muoiono per la loro performance hit / miss. La funzionalità di base di una cache è che se i dati richiesti sono disponibili nella cache (un hit), verranno recuperati dalla cache e non dall'archivio dati principale. Nel design originale del HybridDataLayer, l'accesso alla cache è stato effettuato tramite metodi privati.

Le regole di visibilità su go rendono impossibile chiamarle direttamente o sostituirle da un altro pacchetto. Per abilitare il test della cache, cambierò questi metodi in funzioni pubbliche. Questo va bene perché il codice dell'applicazione reale opera attraverso il DataLayer interfaccia, che non espone tali metodi.

Il codice di prova, tuttavia, sarà in grado di sostituire queste funzioni pubbliche secondo necessità. Innanzitutto, aggiungiamo un metodo per ottenere l'accesso al client Redis, in modo che possiamo manipolare la cache:

func (m * HybridDataLayer) GetRedis () * redis.Client return m.redis 

Quindi cambierò il getSongByUser_DB () metodi a una variabile di funzione pubblica. Ora, nel test, posso sostituire il GetSongsByUser_DB () variabile con una funzione che tiene traccia di quante volte è stato chiamato e poi lo inoltra alla funzione originale. Questo ci permette di verificare se una chiamata a GetSongsByUser () recuperato i brani dalla cache o dal DB. 

Scopriamolo pezzo per pezzo. Innanzitutto, otteniamo il livello dati (che cancella anche il DB e i redis), creiamo un utente e aggiungiamo un brano. Il AddSong () il metodo popola anche i redis. 

func TestGetSongsByUser_Cache (t * testing.T) now: = time.Now () u: = Utente Nome: "Gigi", Email: "[email protected]", RegisteredAt: now, LastLogin: now dl, err : = getDataLayer () se err! = nil t.Error ("Impossibile creare il livello dati ibrido") err = dl.CreateUser (u) se err! = nil t.Error ("Impossibile creare l'utente")  lm, err: = NewSongManager (u, dl) se err! = nil t.Error ("NewSongManager () ha restituito 'nil'") err = lm.AddSong (testSong, nil) se err! = nil t .Error ("AddSong () failed") 

Questa è la parte interessante. Mantengo la funzione originale e definisco una nuova funzione strumentata che incrementa il locale callCount variabile (è tutto in una chiusura) e chiama la funzione originale. Quindi, assegno la funzione strumentata alla variabile GetSongsByUser_DB. D'ora in poi, ogni chiamata dal livello dati ibrido a GetSongsByUser_DB () andrà alla funzione strumentata.     

 callCount: = 0 originalFunc: = GetSongsByUser_DB instrumentedFunc: = func (m * HybridDataLayer, stringa email, song * [] Song) (errore err) callCount + = 1 return originalFunc (m, email, brani) GetSongsByUser_DB = instrumentedFunc 

A questo punto, siamo pronti per testare realmente l'operazione della cache. Innanzitutto, il test chiama il GetSongsByUser () del SongManager che lo inoltra al livello dati ibrido. La cache dovrebbe essere compilata per questo utente che abbiamo appena aggiunto. Quindi il risultato atteso è che la nostra funzione strumentata non sarà chiamata, e il callCount rimarrà a zero.

 _, err = lm.GetSongsByUser (u) se err! = nil t.Error ("GetSongsByUser () failed") // Verifica che il DB non sia stato acceduto perché la cache dovrebbe essere // popolata da AddSong () se callCount > 0 t.Error ('GetSongsByUser_DB () chiamato quando non dovrebbe avere') 

L'ultimo test case è quello di garantire che se i dati dell'utente non sono nella cache, verranno recuperati correttamente dal DB. Il test lo realizza lavando Redis (cancellando tutti i suoi dati) e facendo un'altra chiamata a GetSongsByUser (). Questa volta, verrà chiamata la funzione instrumentata e il test verifica che il callCount è uguale a 1. Infine, l'originale GetSongsByUser_DB () la funzione è ripristinata.

 // Cancella la cache dl.GetRedis (). FlushDB () // Ottieni di nuovo i brani, ora dovrebbe andare nel DB // perché la cache è vuota _, err = lm.GetSongsByUser (u) se err! = Nil t.Error ("GetSongsByUser () failed") // Verifica dell'accesso al database perché la cache è vuota se callCount! = 1 t.Error ('GetSongsByUser_DB () non è stato chiamato una volta come dovrebbe')  GetSongsByUser_DB = originalFunc

Invalidazione della cache

La nostra cache è molto semplice e non fa alcuna invalidazione. Funziona molto bene fino a quando tutte le canzoni vengono aggiunte attraverso il AddSong () metodo che si occupa dell'aggiornamento di Redis. Se aggiungiamo più operazioni come la rimozione di brani o l'eliminazione di utenti, queste operazioni dovrebbero occuparsi dell'aggiornamento di Redis di conseguenza.

Questa cache molto semplice funziona anche se disponiamo di un sistema distribuito in cui più macchine indipendenti possono eseguire il nostro servizio Songify, a condizione che tutte le istanze funzionino con le stesse istanze DB e Redis.

Tuttavia, se DB e cache non riescono a sincronizzarsi a causa di operazioni di manutenzione o di altri strumenti e applicazioni che cambiano i nostri dati, è necessario elaborare una politica di invalidazione e aggiornamento per la cache. Può essere testato utilizzando le stesse tecniche: sostituire le funzioni di destinazione o accedere direttamente al DB e ai Redis nel test per verificare lo stato.

Cache LRU

Di solito, non puoi lasciare che la cache cresca all'infinito. Uno schema comune per conservare i dati più utili nella cache sono le cache LRU (utilizzate meno di recente). I dati più vecchi vengono espulsi dalla cache quando raggiunge la capacità.

Il test implica l'impostazione della capacità a un numero relativamente piccolo durante il test, il superamento della capacità e la garanzia che i dati più vecchi non siano più nella cache e che l'accesso richieda l'accesso al DB. 

Testare l'integrità dei dati

Il tuo sistema è buono quanto l'integrità dei tuoi dati. Se hai dati corrotti o dati mancanti, allora sei in cattive condizioni. Nei sistemi reali, è difficile mantenere una perfetta integrità dei dati. Schema e formati cambiano, i dati vengono ingeriti attraverso canali che potrebbero non controllare tutti i vincoli, i bug rilasciano dati errati, gli amministratori tentano correzioni manuali, backup e ripristini potrebbero essere inaffidabili.

Data questa dura realtà, è necessario testare l'integrità dei dati del sistema. Testare l'integrità dei dati è diverso rispetto ai normali test automatici dopo ogni cambio di codice. Il motivo è che i dati possono andare male anche se il codice non cambia. È assolutamente necessario eseguire i controlli di integrità dei dati dopo le modifiche al codice che potrebbero alterare la memorizzazione o la rappresentazione dei dati, ma anche eseguirli periodicamente.

Test dei vincoli

I vincoli sono il fondamento della modellazione dei dati. Se si utilizza un DB relazionale, è possibile definire alcuni vincoli a livello SQL e lasciare che il DB li imponga. Nullness, lunghezza dei campi di testo, unicità e relazioni 1-N possono essere definite facilmente. Ma SQL non può controllare tutti i vincoli.

Ad esempio, in Desongcious esiste una relazione N-N tra utenti e brani. Ogni canzone deve essere associata ad almeno un utente. Non c'è un buon modo per far rispettare questo in SQL (beh, si può avere una chiave esterna dalla canzone all'utente e fare in modo che la canzone punti ad uno degli utenti ad essa associati). Un altro vincolo potrebbe essere che ogni utente può avere al massimo 500 brani. Ancora una volta, non c'è modo di rappresentarlo in SQL. Se si utilizzano archivi di dati NoSQL, in genere esiste ancora meno supporto per la dichiarazione e la convalida dei vincoli a livello di archivio dati.

Questo ti lascia un paio di opzioni:

  • Garantire che l'accesso ai dati avvenga solo attraverso interfacce e strumenti controllati che applichino tutti i vincoli.
  • Esegui periodicamente la scansione dei dati, cerchi le violazioni dei vincoli e correggili.    

Test dell'idempotenza

Idempotenza significa che eseguire la stessa operazione più volte di seguito avrà lo stesso effetto di eseguirlo una volta. 

Ad esempio, l'impostazione della variabile da x a 5 è idempotente. È possibile impostare da x a 5 una volta o un milione di volte. Sarà comunque 5. Tuttavia, l'incremento di X per 1 non è idempotente. Ogni incremento consecutivo cambia il suo valore. L'idempotency è una proprietà molto auspicabile in sistemi distribuiti con partizioni di rete temporanee e protocolli di recupero che riprovano a inviare un messaggio più volte se non vi è una risposta immediata.

Se si progetta l'idempotenzialità nel codice di accesso ai dati, è necessario testarlo. Questo è in genere molto facile. Per ogni operazione idempotent estendi per eseguire l'operazione due volte o più di seguito e verifica che non vi siano errori e lo stato rimanga lo stesso.   

Si noti che la progettazione idempotente può a volte nascondere gli errori. Prendi in considerazione l'eliminazione di un record da un DB. È un'operazione idempotente. Dopo aver eliminato un record, il record non esiste più nel sistema e tentare di eliminarlo di nuovo non lo ripristinerà. Ciò significa che provare a eliminare un record inesistente è un'operazione valida. Ma potrebbe mascherare il fatto che la chiave di registrazione sbagliata sia stata passata dal chiamante. Se si restituisce un messaggio di errore, allora non è idempotente.    

Test delle migrazioni dei dati

Le migrazioni dei dati possono essere operazioni molto rischiose. A volte si esegue uno script su tutti i dati o parti critiche dei dati e si esegue un intervento chirurgico serio. Dovresti essere pronto con il piano B nel caso in cui qualcosa vada storto (ad esempio, torna ai dati originali e scopri cosa è andato storto).

In molti casi, la migrazione dei dati può essere un'operazione lenta e costosa che potrebbe richiedere due sistemi affiancati per la durata della migrazione. Ho partecipato a diverse migrazioni di dati che hanno richiesto diversi giorni o addirittura settimane. Di fronte a una massiccia migrazione dei dati, vale la pena investire il tempo e testare la migrazione stessa su un piccolo (ma rappresentativo) sottoinsieme dei dati e quindi verificare che i dati appena migrati siano validi e che il sistema possa funzionare con esso. 

Test dei dati mancanti

I dati mancanti sono un problema interessante. A volte i dati mancanti violano l'integrità dei tuoi dati (ad esempio, una canzone di cui manca l'utente), ea volte manca solo (ad esempio, qualcuno rimuove un utente e tutte le sue canzoni).

Se i dati mancanti causano un problema di integrità dei dati, li rileverai nei test di integrità dei dati. Tuttavia, se mancano alcuni dati, non esiste un modo semplice per rilevarli. Se i dati non sono mai stati archiviati in memoria persistente, potrebbe esserci una traccia nei log o in altri negozi temporanei.

A seconda della quantità di dati mancanti a rischio, è possibile scrivere alcuni test che rimuovono deliberatamente alcuni dati dal sistema e verificare che il sistema si comporti come previsto.

Conclusione

Il test del codice ad alta intensità di dati richiede una pianificazione deliberata e una comprensione dei requisiti di qualità. Puoi testare a diversi livelli di astrazione e le tue scelte influiranno sulla completezza e completezza dei test, su quanti aspetti del tuo livello dati reale testerai, sulla velocità di esecuzione dei test e su quanto sia facile modificare i test quando cambiamenti del livello dati.

Non esiste un'unica risposta corretta. Devi trovare il tuo punto debole lungo lo spettro da test completi, lenti e laboriosi a test veloci e leggeri.