Test di codice intensivo dei dati con Go, parte 3

Panoramica

Questa è la terza parte su cinque in una serie di tutorial sul test del codice ad alta intensità di dati con Go. Nella seconda parte, ho coperto i test con un vero strato di dati in memoria basato sul popolare SQLite. In questo tutorial, esaminerò un livello dati complesso locale che include un DB relazionale e una cache Redis.

Test contro un livello dati locale

Testare contro un livello dati in memoria è fantastico. I test sono velocissimi e tu hai il pieno controllo. Ma a volte è necessario essere più vicini alla configurazione effettiva del livello dati di produzione. Ecco alcuni possibili motivi:

  • Usi dettagli specifici del tuo DB relazionale che vuoi testare.
  • Il tuo livello dati è composto da diversi archivi di dati interagenti.
  • Il codice in prova è costituito da diversi processi che accedono allo stesso livello dati.
  • Si desidera preparare o osservare i dati del test utilizzando strumenti standard.
  • Non si desidera implementare un livello dati dedicato in memoria se il livello dati è in flusso.
  • Vuoi solo sapere che stai testando il tuo attuale livello di dati.
  • È necessario testare con un sacco di dati che non si adattano alla memoria.

Sono sicuro che ci sono altri motivi, ma puoi capire perché l'utilizzo di un livello dati in memoria per i test potrebbe non essere sufficiente in molti casi.

OK. Quindi vogliamo testare un livello dati reale. Ma vogliamo ancora essere il più leggeri e agili possibile. Ciò significa un livello dati locale. Ecco i vantaggi:

  • Non è necessario eseguire il provisioning e configurare nulla nel data center o nel cloud.
  • Non è necessario preoccuparsi dei nostri test che corrompono i dati di produzione per sbaglio.
  • Non c'è bisogno di coordinarsi con altri sviluppatori in un ambiente di test condiviso. 
  • Nessuna lentezza sulle chiamate di rete.
  • Pieno controllo sul contenuto del livello dati, con la possibilità di ripartire da zero in qualsiasi momento.  

In questo tutorial alzeremo la posta. Implementeremo (in modo molto parziale) un livello dati ibrido costituito da un DB relazionale MariaDB e un server Redis. Quindi useremo Docker per alzare un livello dati locale che possiamo usare nei nostri test. 

Utilizzo di Docker per evitare mal di testa per l'installazione

Per prima cosa, hai bisogno di Docker, ovviamente. Controlla la documentazione se non hai familiarità con Docker. Il passo successivo è quello di ottenere immagini per i nostri archivi dati: MariaDB e Redis. Senza entrare troppo nel dettaglio, MariaDB è un ottimo DB relazionale compatibile con MySQL e Redis è un ottimo archivio di valori-chiave in memoria (e molto altro). 

> docker pull mariadb ...> docker pull redis ...> immagini docker TAG REPOSITORY ID IMMAGINE CREATO TAGLIA mariadb latest 51d6a5e69fa7 2 settimane fa 402MB redis latest b6dddb991dfa 2 settimane fa 107MB 

Ora che abbiamo installato Docker e abbiamo le immagini per MariaDB e Redis, possiamo scrivere un file docker-compose.yml, che useremo per lanciare i nostri archivi di dati. Chiamiamo il nostro DB "songify".

mariadb-songify: immagine: mariadb: ultimo comando:> --general-log --general-log-file = / var / log / mysql / query.log espone: - Porte "3306": - "3306: 3306" ambiente : MYSQL_DATABASE: "songify" MYSQL_ALLOW_EMPTY_PASSWORD: "true" volumes_from: - mariadb-data mariadb-data: image: mariadb: ultimi volumi: - / var / lib / mysql entrypoint: / bin / bash redis: image: redis espone: - " 6379 "porte: -" 6379: 6379 " 

Puoi avviare i tuoi archivi di dati con il docker-comporre up comando (simile a vagabondo). L'output dovrebbe assomigliare a questo: 

> docker-compose up Starting hybridtest_redis_1 ... Starting hybridtest_mariadb-data_1 ... Starting hybridtest_redis_1 Starting hybridtest_mariadb-data_1 ... done Starting hybridtest_mariadb-songify_1 ... Starting hybridtest_mariadb-songify_1 ... done Collegamento a hybridtest_mariadb-data_1, hybridtest_redis_1, hybridtest_mariadb-songify_1 ... redis_1 | * DB caricato dal disco: 0.002 secondi redis_1 | * Pronto ad accettare connessioni ... mariadb-songify_1 | [Nota] mysqld: pronto per le connessioni ... 

A questo punto, si ha un server MariaDB a pieno titolo in ascolto sulla porta 3306 e un server Redis in ascolto sulla porta 6379 (entrambe sono le porte standard).

Il livello dati ibrido

Approfitta di questi potenti archivi di dati e aggiorna il nostro livello dati a un livello dati ibrido che memorizza nella cache i brani per utente in Redis. quando GetSongsByUser ()viene chiamato, il livello dati controlla prima se Redis memorizza già i brani per l'utente. Se invece restituisce i brani da Redis, se non lo fa (cache miss), recupererà i brani da MariaDB e popolerà la cache Redis, quindi sarà pronto per la prossima volta. 

Ecco la definizione di struct e constructor. La struttura mantiene un handle DB come prima e anche un client redis. Il costruttore si connette al DB relazionale e ai Redis. Crea lo schema e svuota i redis solo se i parametri corrispondenti sono veri, che è necessario solo per il test. In produzione, si crea lo schema una sola volta (ignorando le migrazioni dello schema).

digita HybridDataLayer struct db * sql.DB redis * redis.Client func NewHybridDataLayer (dbHost stringa, dbPort int, redisHost stringa, createSchema bool, clearRedis bool) (* HybridDataLayer, errore) dsn: = fmt.Sprintf ("root @ tcp (% s:% d) / ", dbHost, dbPort) se createSchema err: = createMariaDBSchema (dsn) se err! = nil return nil, err db, err: = sql.Open (" mysql ", dsn + "desongcious? parseTime = true") se err! = nil return nil, err redisClient: = redis.NewClient (& redis.Options Addr: redisHost + ": 6379", Password: "", DB: 0, ) _, err = redisClient.Ping (). Result () se err! = nil return nil, err se clearRedis redisClient.FlushDB () return & HybridDataLayer db, redisClient, nil

Utilizzando MariaDB

MariaDB e SQLite sono un po 'diversi per quanto riguarda il DDL. Le differenze sono piccole, ma importanti. Go non ha un toolkit cross-DB maturo come il fantastico SQLAlchemy di Python, quindi devi gestirlo da solo (no, Gorm non conta). Le principali differenze sono:

  • Il driver SQL è "github.com/go-sql-driver/mysql".
  • Il database non vive in memoria, quindi viene ricreato ogni volta (rilascia e crea). 
  • Lo schema deve essere una porzione di istruzioni DDL indipendenti anziché una stringa di tutte le istruzioni.
  • Le chiavi primarie autoincrementanti sono contrassegnate da INCREMENTO AUTOMATICO.
  • VARCHAR invece di TESTO.

Ecco il codice:

func createMariaDBSchema (dsn string) error db, err: = sql.Open ("mysql", dsn) se err! = nil return err // Ricrea comandi DB: = [] string "DROP DATABASE songify;", "CREATE DATABASE songify;", per _, s: = range (comandi) _, err = db.Exec (s) se err! = Nil return err // Crea schema db, err = sql.Open ("mysql", dsn + "songify? parseTime = true") se err! = nil return err schema: = [] string 'CREATE TABLE IF NOT EXISTS song (id INTEGER PRIMARY KEY AUTO_INCREMENT, url VARCHAR (2088) UNIQUE , titolo VARCHAR (100), descrizione VARCHAR (500)); ',' CREATE TABLE SE NON ESISTE utente (id INTEGER PRIMARY KEY AUTO_INCREMENT, nome VARCHAR (100), email VARCHAR (100) UNIQUE, registered_at TIMESTAMP, last_login TIMESTAMP); ', "CREATE INDEX user_email_idx ON utente (email);",' CREATE TABLE IF NOT EXISTS label (id INTEGER PRIMARY KEY AUTO_INCREMENT, nome VARCHAR (100) UNIQUE); ', "CREATE INDEX label_name_idx ON label (name);", 'CREATE TABLE SE NON ESISTE label_song (label_id INTEGER NOT NULL REFE Etichetta RENCES (id), song_id INTEGER NOT NULL REFERENCES song (id), PRIMARY KEY (label_id, song_id)); ',' CREATE TABLE SE NON ESISTE user_song (user_id INTEGER NOT NULL REFERENCES utente (id), song_id INTEGER NOT NULL REFERENCES song (id), PRIMARY KEY (user_id, song_id)); ', per _, s: = range (schema) _, err = db.Exec (s) se err! = nil return err return nil 

Uso di Redis

Redis è molto facile da usare da Go. La libreria client "github.com/go-redis/redis" è molto intuitiva e segue fedelmente i comandi di Redis. Ad esempio, per verificare se esiste una chiave, basta usare il Uscite () metodo del client redis, che accetta una o più chiavi e restituisce quante di esse esistono. 

In questo caso, controllo solo una chiave:

 count, err: = m.redis.Exists (email) .Result () se err! = nil return err

Test dell'accesso a più archivi di dati

I test sono in realtà identici. L'interfaccia non è cambiata e il comportamento non è cambiato. L'unico cambiamento è che l'implementazione ora mantiene una cache in Redis. Il GetSongsByEmail () il metodo ora chiama refreshUser_Redis ().

func (m * HybridDataLayer) GetSongsByUser (u User) (songs [] Song, err error) err = m.refreshUser_Redis (e.mail, e canzoni) return 

Il refreshUser_Redis () metodo restituisce le canzoni dell'utente da Redis se esistono e in caso contrario le preleva da MariaDB.

digita Songs * [] Song func (m * HybridDataLayer) refreshUser_Redis (stringa email, out Songs) error count, err: = m.redis.Exists (email) .Result () se err! = nil return err se conta == 0 err = m.getSongsByUser_DB (email, out) se err! = Nil return err per _, song: = range * out s, err: = serializeSong (song) se err! = Nil return err  _, err = m.redis.SAdd (email, s) .Result () se err! = nil return err return membri, err: = m.redis.SMembers (email) .Result () per _ , membro: = range members song, err: = deserializeSong ([] byte (membro)) se err! = nil return err * out = append (* out, song) return out, nil 

C'è un piccolo problema qui da un punto di vista della metodologia di test. Quando testiamo l'interfaccia del livello dati astratti, non abbiamo visibilità nell'implementazione del livello dati.

Ad esempio, è possibile che ci sia un grande difetto in cui il livello dati ignora completamente la cache e recupera sempre i dati dal DB. I test passeranno, ma non trarremo beneficio dalla cache. Nella parte 5 parlerò della verifica della cache, che è molto importante.  

Conclusione

In questo tutorial, abbiamo coperto i test con un livello dati complesso locale costituito da più archivi di dati (un DB relazionale e una cache Redis). Abbiamo anche utilizzato Docker per distribuire facilmente più archivi di dati per i test.

Nella parte quattro, ci concentreremo sui test sugli archivi di dati remoti, utilizzando le istantanee dei dati di produzione per i nostri test e generando anche i nostri dati di test. Rimanete sintonizzati!