Testing av dataintensiv kode med Go, del 2

Oversikt

Dette er del to av fem i en opplæringsserie om testing av datakrevende kode. I del ett dekket jeg utformingen av et abstrakt datalag som muliggjør riktig testing, hvordan man håndterer feil i datalaget, hvordan man mocker dataadgangskoden og hvordan man tester mot et abstrakt datalag. I denne opplæringen vil jeg gå over testing mot et ekte minne lagringsdata basert på den populære SQLite. 

Testing mot en In-Memory Data Store

Testing mot et abstrakt datalag er flott for noen brukstilfeller hvor du trenger mye presisjon, du forstår nøyaktig hva som kaller koden som testes, skal gjøre mot datalaget, og du er ok med å forberede de svarte svarene.

Noen ganger er det ikke så lett. Serien av samtaler til datalaget kan være vanskelig å finne, eller det krever mye arbeid for å forberede riktige hermetiske svar som er gyldige. I disse tilfellene kan det hende du må jobbe mot en dataforretning i minnet. 

Fordelene ved en dataforretning i minnet er:

  • Det er veldig fort. 
  • Du jobber mot en faktisk datalager.
  • Du kan ofte fylle den fra bunnen av med filer eller kode.

Spesielt hvis datalageret er en relationsdatabase, er SQLite et fantastisk alternativ. Bare husk at det er forskjeller mellom SQLite og andre populære relationsdatabaser som MySQL og PostgreSQL.

Pass på at du står for det i tester. Vær oppmerksom på at du fortsatt får tilgang til dataene dine via det abstrakte datalaget, men nå er lagringsenheten under tester i minnet datalager. Din test vil fylle testdataene annerledes, men koden under test er lykkelig uvitende om hva som skjer.

Bruke SQLite

SQLite er en innebygd DB (koblet til søknaden din). Det er ingen separat DB-server som kjører. Det lagrer vanligvis dataene i en fil, men har også muligheten til en lagringsplass i minnet. 

Her er InMemoryDataStore struct. Det er også en del av concrete_data_layer pakke, og importerer go-sqlite3 tredjeparts pakken som implementerer standard Golang "database / sql" grensesnitt.  

pakke concrete_data_layer import ("database / sql". "abstract_data_layer" _ "github.com/mattn/go-sqlite3" "time" "fmt") skriv InMemoryDataLayer struktur db * sql.DB

Konstruerer In-Memory Data Layer

De NewInMemoryDataLayer () constructor-funksjonen oppretter en in-memory sqlite DB og returnerer en peker til InMemoryDataLayer

funk NewInMemoryDataLayer () (* InMemoryDataLayer, feil) db, err: = sql.Open ("sqlite3", ": minne:") hvis err! = null return null, err err = createSqliteSchema (db) returnere & InMemoryDataLayer  db, null 

Merk at hver gang du åpner et nytt ": minne:" DB, starter du fra grunnen av. Hvis du vil ha utholdenhet over flere anrop til NewInMemoryDataLayer (), du bør bruke fil :: minne: cache = delt. Se denne GitHub diskusjonstråden for mer informasjon.

De InMemoryDataLayer implementerer layer grensesnitt og lagrer faktisk dataene med riktige forhold i sin SQL-database. For å gjøre det, må vi først lage et skikkelig skjema, som akkurat er jobben til createSqliteSchema () fungere i konstruktøren. Den lager tre datatabeller-sang-, bruker- og etikett- og to kryssreferansetabeller, label_song og user_song.

Det legger til noen begrensninger, indekser og utenlandske nøkler for å forholde tabellene til hverandre. Jeg vil ikke dvele på de spesifikke detaljene. Hovedpunktet er at hele skjemaet DDL er erklært som en enkelt streng (bestående av flere DDL-setninger) som deretter utføres ved bruk av db.Exec () metode, og hvis noe går galt, returnerer det en feil. 

func createSqliteSchema (db * sql.DB) feil schema: = 'CREATE TABLE IF IKKE EXISTS sang (ID INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, navn TEXT, beskrivelse TEXT); CREATE TABLE IF NOT EXISTS bruker (id INTEGER PRIMARY KEY AUTOINCREMENT, navn TEKST, e-post TEKST UNIK, registrert_at TIMESTAMP, last_login TIMESTAMP); CREATE INDEX user_email_idx PÅ bruker (e-post); Opprett tabell hvis ikke EXISTS-etikett (ID INTEGER PRIMARY KEY AUTOINCREMENT, navn TEKST UNIKT); CREATE INDEX label_name_idx ON label (navn); CREATE TABLE IF NOT EXISTS label_song (label_id INTEGER NOT NULL REFERENCES label (id), song_id INTEGER IKKE NULL REFERENSER sang (id), PRIMARY KEY (label_id, song_id)); Opprett tabell hvis ikke EXISTS user_song (user_id INTEGER IKKE NULL REFERENSER bruker (id), song_id INTEGER IKKE NULL REFERENSER sang (id), PRIMARY KEY (user_id, song_id)); ' _, err: = db.Exec (skjema) retur feil 

Det er viktig å innse at mens SQL er standard, har hvert databasebehandlingssystem (DBMS) sin egen smak, og den nøyaktige skjemadefinisjonen vil ikke nødvendigvis fungere som for en annen DB.

Implementere In-Memory Data Layer

For å gi deg en smak av implementeringsinnsatsen til et datainnhold i minnet, er det et par metoder: AddSong () og GetSongsByUser ()

De AddSong () Metoden gjør mye arbeid. Det legger inn en plate i sang bord og inn i hver av referansetabellene: label_song og user_song. På hvert punkt, hvis en operasjon mislykkes, returnerer den bare en feil. Jeg bruker ikke noen transaksjoner fordi den bare er laget for test, og jeg er ikke bekymret for delvise data i DB.

func (m * InMemoryDataLayer) AddSong (bruker Bruker, sang Song, etiketter [] Etikett) feil s: = 'INSERT INTO sang (url, navn, beskrivelse) verdier (?,?,?)' erklæring, err: = m .dbPrepare (s) hvis err! = null return err result, err: = statement.Exec (sang.Url, sang.navn, sang.Description) hvis err! = null return err songId, err: = result.LastInsertId () hvis err! = null return err s = "VELG ID fra bruker der e-post =?" rader, feil: = m.db.Query (s, user.Email) hvis err! = null return err var userId int for rader.Next () err = raws.Scan (& userId) hvis feil! = null  returner err s = 'INSERT i bruker_song (user_id, song_id) verdier (?,?)' err = m.dbPrepare (s) hvis err! = null return err _, err = statement.Exec (userId, songId) hvis err! = null return err var labelId int64 s: = "INSERT INTO etikett (navn) verdier (?)" label_ins, err: = m.db.Prepare (er) hvis feil! = null return err s = 'INSERT i label_song (label_id, song_id) verdier (?,?)' label_song_ins, err: = m.dbPrepare (s) hvis err! = null return err for _, t: = rekkevidde etiketter s = "VELG ID fra etikett hvor navn =?" rader, feil: = m.db.Query (s, t.Name) hvis err! = null return err labelId = -1 for rader .Next () err = raws.Scan (& labelId) hvis feil! = null return err hvis labelId == -1 result, err = label_ins.Exec (t.Name) hvis err! = null return err labelId, err = result.LastInsertId () hvis feil! = null retur feil  resultat, err = label_song_ins.Exec (labelId, songId) hvis feil! = null return err returnér null 

De GetSongsByUser () bruker en delta + subselect fra user_song kryssreferanse for å returnere sanger til en bestemt bruker. Den bruker Spørsmål() metoder og senere skanner hver rad for å fylle a Sang struktur fra domenemodellmodellen og returner et stykke sanger. Implementeringen på lavt nivå som en relasjons DB er skjult sikkert.

func (m * InMemoryDataLayer) GetSongsByUser (u Bruker) ([] Sang, feil) s: = 'VELG URL, tittel, beskrivelse FRA sang L INNER JOIN user_song UL ON UL.song_id = L.ID HVOR UL.user_id = SELECT ID fra bruker WHERE email =?) 'Rader, err: = m.db.Query (s, u.Email) hvis err! = Null return null, err for rader.Neste () var sang Song err = rows.Scan (& song.Url, & song.Title, & song.Description) hvis err! = null return nil, err songs = append (sanger, sang) returner sanger, null 

Dette er et godt eksempel på å utnytte en ekte relasjonell DB som sqlite for implementering av minnet datalagring vs. rullende oss selv, som ville kreve å holde kart og sikre at alle bokføringene var korrekte. 

Kjører tester mot SQLite

Nå som vi har et riktig data lag i minnet, la oss ta en titt på testene. Jeg plasserte disse testene i en separat pakke som heter sqlite_test, og jeg importerer lokalt det abstrakte datalaget (domenemodellen), det konkrete datalaget (for å lage data lagret i minnet) og sanglederen (koden under testen). Jeg lager også to sanger for testene fra den sensasjonelle panamanske kunstneren El Chombo!

pakke sqlite_test import ("testing". "abstract_data_layer". "concrete_data_layer". "song_manager") const (url1 = "https://www.youtube.com/watch?v=MlW7T0SUH0E" url2 = "https: // www. youtube.com/watch?v=cVFDlg4pbwM ") var testSong = Sang Url: url1, Navn:" Chakaron " var testSong2 = Sang Url: url2, Navn:" El Gato Volador " 

Testmetoder oppretter et nytt lagringslag for data i minnet for å starte fra grunnen og kan nå ringe metoder i datalaget for å forberede testmiljøet. Når alt er satt opp, kan de påkalle sangadministratormetoder og senere verifisere at datalaget inneholder forventet tilstand.

For eksempel, AddSong_Success () testmetode lager en bruker, legger til en sang ved hjelp av sanglederens AddSong () metode, og verifiserer at senere ringer GetSongsByUser () returnerer den tilførte sangen. Den legger til en annen sang og bekrefter igjen.

Funk TestAddSong_Success (t * testing.T) u: = Bruker Navn: "Gigi", E-post: "[email protected]" dl, err: = NewInMemoryDataLayer () hvis err! = null t.Error Kunne ikke opprette data i minnet ") err = dl.CreateUser (u) hvis feil! = Null t.Error (" Kunne ikke opprette bruker ") lm, err: = NewSongManager (u, dl) hvis feil ! = nil t.Error ("NewSongManager () returnert nil") err = lm.AddSong (testSong, null) hvis feil! = null t.Error ("AddSong () mislyktes") sanger, feil : = dl.GetSongsByUser (u) hvis feil! = null t.Error ("GetSongsByUser () mislyktes") hvis len (sanger)! = 1 t.Error ('GetSongsByUser () returnerte ikke en sang som forventet ') hvis sanger [0]! = testSong t.Error ("Lagt sang samsvarer ikke med innsangssang") // Legg til en annen sang err = lm.AddSong (testSong2, null) hvis feil! = null  t.rror ("AddSong () mislyktes") sanger, err = dl.GetSongsByUser (u) hvis feil! = null t.Error ("GetSongsByUser () mislyktes") hvis len (sanger)! = 2 t .Error ('GetSongsByUser () returnerte ikke to sanger som forventet') hvis sanger [0]! = TestSong t.Error ("Lagt sang stemmer ikke med inntaks sang ") hvis sanger [1]! = testSong2 t.Error (" Lagt sang samsvarer ikke med inntaks sang ") 

De TestAddSong_Duplicate () testmetode er lik, men i stedet for å legge til en ny sang andre gang, legger den til samme sang, noe som resulterer i en duplikat sangfeil:

 u: = Bruker Navn: "Gigi", E-post: "[email protected]" dl, err: = NewInMemoryDataLayer () hvis err! = null t.Error ("Kunne ikke lage lagringsdata i minnet")  err = dl.CreateUser (u) hvis err! = null t.Error ("Kunne ikke opprette bruker") lm, err: = NewSongManager (u, dl) hvis feil! = null t.Error ("NewSongManager () returnert 'nil' ") err = lm.AddSong (testSong, null) hvis feil! = null t.Error (" AddSong () mislyktes ") sanger, feil: = dl.GetSongsByUser (u) hvis feil ! = nil t.Error ("GetSongsByUser () mislyktes") hvis len (sanger)! = 1 t.Error ('GetSongsByUser () returnerte ikke en sang som forventet') hvis sanger [0]! = testSong t.Error ("Lagt sang samsvarer ikke med inntaks sang") // Legg til samme sang igjen err = lm.AddSong (testSong, null) hvis feil == nil t.Error ('AddSong () burde ha mislyktes for en duplikat sang ') expectedErrorMsg: = "Dupliser sang" errorMsg: = err.Error () hvis errorMsg! = expectedErrorMsg t.Error (' AddSong () returnerte feil feilmelding for duplikat sang '

Konklusjon

I denne opplæringen implementerte vi et lagringsdata lagret basert på SQLite, befolket en SQLite-database i minnet med testdata, og benyttet data lagret i minnet for å teste applikasjonen.

I del tre vil vi fokusere på testing mot et lokalt komplekst datalag som består av flere datalager (en relasjons DB og en Redis cache). Følg med.