Testing av dataintensiv kod med go, del 2

Översikt

Detta är del två av fem i en tutorial-serie om testning av dataintensiv kod. I del ett täckte jag utformningen av ett abstrakt datalag som möjliggör korrekt testning, hur man hanterar fel i datalagret, hur man mockar dataåtkomstkoden och hur man testar mot ett abstrakt datalager. I den här handledningen går jag över testning mot ett realt minne i minnet baserat på den populära SQLite. 

Testa mot en minnesdatabutik

Att testa mot ett abstrakt datalager är bra för vissa användningsfall där du behöver mycket precision, du förstår exakt vad som kallas koden som testas kommer att göra mot datalaget och du är okej med att förbereda de svåra svaren.

Ibland är det inte så lätt. Serien av samtal till datalagret kan vara svårt att räkna, eller det krävs en stor insats för att förbereda korrekta behållarresponser som är giltiga. I dessa fall kan du behöva arbeta mot en minnesdatabutik. 

Fördelarna med en minnesdatabutik är:

  • Det är väldigt snabbt. 
  • Du arbetar mot en faktisk datalager.
  • Du kan ofta fylla den från början med hjälp av filer eller kod.

Särskilt om din datalagring är en relationell DB är SQLite ett fantastiskt alternativ. Kom bara ihåg att det finns skillnader mellan SQLite och andra populära relationsdatabaser som MySQL och PostgreSQL.

Se till att du tar reda på det i dina test. Observera att du fortfarande har tillgång till dina data genom det abstrakta datalagret, men nu är lagringsutrymmet under testen minnesdatabutiken. Ditt test kommer att fylla testdata på olika sätt, men koden som testas är lyckligt omedveten om vad som händer.

Använda SQLite

SQLite är en inbäddad DB (kopplad till din ansökan). Det finns ingen separat DB-server som körs. Det lagrar vanligtvis data i en fil, men har också möjlighet till en minnesbakgrundsbutik. 

Här är InMemoryDataStore struct. Det är också en del av concrete_data_layer paketet, och det importerar go-sqlite3-tredjepartspaketet som implementerar standard Golang "database / sql" -gränssnittet.  

paketet concrete_data_layer import ("databas / sql". "abstract_data_layer" _ "github.com/mattn/go-sqlite3" "time" "fmt") typ InMemoryDataLayer struktur db * sql.DB

Konstruera In Memory Data Layer

De NewInMemoryDataLayer () konstruktörfunktionen skapar ett SQL-minne i minnet och returnerar en pekare till InMemoryDataLayer

func NewInMemoryDataLayer () (* InMemoryDataLayer, error) db, err: = sql.Open ("sqlite3", ": minne:") om err! = nil return null, err err = createSqliteSchema (db) returnera & InMemoryDataLayer  db, nil 

Observera att varje gång du öppnar ett nytt ": minne:" DB startar du från början. Om du vill ha persistens över flera samtal till NewInMemoryDataLayer (), du borde använda fil :: minne: cache = delat. Se denna GitHub-diskussionsgänga för mer information.

De InMemoryDataLayer implementerar datalayer gränssnittet och lagrar faktiskt data med korrekta relationer i sin SQL-databas. För att göra det måste vi först skapa ett riktigt schema, vilket är precis det jobb som createSqliteSchema () funktion i konstruktören. Det skapar tre datatabeller-sång, användare och etikett- och två referens tabeller, label_song och user_song.

Det lägger till några begränsningar, index och utländska nycklar för att relatera tabellerna till varandra. Jag kommer inte att dölja de specifika detaljerna. Huvuddelen av det är att hela schemat DDL förklaras som en enda sträng (bestående av flera DDL-satser) som sedan exekveras med användning av db.Exec () metod, och om något går fel, returnerar det ett fel. 

func createSqliteSchema (db * sql.DB) fel schema: = 'CREATE TABLE IF INTE EXISTS song (id INTEGER PRIMARY KEY AUTOINCREMENT, URL TEXT UNIQUE, namn TEXT, beskrivning TEXT); Skapa tabell om inte EXISTS användare (ID INTEGER PRIMARY KEY AUTOINCREMENT, namn TEXT, email TEXT UNIQUE, registered_at TIMESTAMP, last_login TIMESTAMP); SKAPA INDEX user_email_idx ON användare (email); Skapa tabell om inte EXISTS-etikett (id INTEGER PRIMARY KEY AUTOINCREMENT, namn TEXT UNIQUE); SKAPA INDEX label_name_idx ON label (namn); CREATE TABLE IF NOT EXISTS label_song (label_id INTEGER NOT NULL REFERENCES etikett (id), song_id INTEGER NOT NULL REFERENCES låt (id), PRIMARY KEY (label_id, song_id)); SKAPA TABELL OM INTE EXISTER user_song (user_id INTEGER NOT NULL REFERENCES användare (id), song_id INTEGER NOT NULL REFERENCES låt (id), PRIMARY KEY (user_id, song_id)) ' _, err: = db.Exec (schema) returnerar err 

Det är viktigt att inse att medan SQL är standard, har varje databashanteringssystem (DBMS) sin egen smak och den exakta schemadefinitionen fungerar inte nödvändigtvis som för en annan DB.

Implementering av minnesdata lagret

För att ge dig en smak av implementeringsinsatsen för ett minnesdataskikt, är det här ett par metoder: AddSong () och GetSongsByUser ()

De AddSong () Metod gör mycket arbete. Det infogar en post i låt bord samt in i varje referens tabeller: label_song och user_song. Vid varje punkt, om någon operation misslyckas, returnerar det bara ett fel. Jag använder inga transaktioner eftersom den endast är avsedd för teständamål, och jag oroar mig inte för partiella data i DB.

func (m * InMemoryDataLayer) AddSong (användaranvändare, låt Song, etiketter [] Etikett) fel s: = 'INSERT INTO låt (url, namn, beskrivning) värden (?,?,?)' uttalande, err: = m .db.Prepare (s) om err! = nil return err result, err: = statement.Exec (song.Url, song.Name, song.Description) om err! = nil return err songId, err: = result.LastInsertId () om err! = nil return err s = "VÄLJ ID FROM användare där email =?" rader, err: = m.db.Query (s, user.Email) om err! = nil return err var användarenId int för rader.Next () err = raws.Scan (& userId) om err! = nil  returnera err s = 'INSERT IN användaren_song (user_id, song_id) värden (?,?)' uttalande, err = m.db.Prepare (s) om err! = nil return err _, err = statement.Exec (userId, songId) om err! = nil return err var labelId int64 s: = "INSERT INTO etikett (namn) värden (?)" label_ins, err: = m.dbPrepare (s) om err! = nil return err s = 'INSERT INTO label_song (label_id, song_id) värden (?,?)' label_song_ins, err: = m.dbPrepare (s) om err! = nil return err för _, t: = intervalletiketter s = "VÄLJ ID FROM label där namn =?" rader, err: = m.db.Query (s, t.Name) om err! = nil return err labelId = -1 för rader.Next () err = raws.Scan (& labelId) om err! = nil return err om labelId == -1 result, err = label_ins.Exec (t.Name) om err! = nil return err labelId, err = result.LastInsertId () om err! = nil return error  resultat, err = label_song_ins.Exec (labelId, songId) om err! = nil return err returnera nil 

De GetSongsByUser () använder en anslutning + undervälj från user_song korsreferens för att returnera låtar till en specifik användare. Den använder Fråga() metoder och sedan skannar varje rad för att fylla a Låt struktur från domänobjektmodellen och returnera en skiva av låtar. Implementeringen på låg nivå som en relationell DB är gömd säkert.

func (m * InMemoryDataLayer) GetSongsByUser (u användare) ([] Song, error) s: = 'VÄLJ url, titel, beskrivning FRÅN låt L INNER JOIN user_song UL ON UL.song_id = L.ID VAR UL.user_id = VÄLJ ID från användare WHERE email =?) 'Rader, err: = m.db.Query (s, u.Email) om err! = Nil return nil, err för rader.Next () var Song Song err = rader.Scan (& song.Url, & song.Title, & song.Description) om err! = nil return nil, err songs = lägg till (låtar, sång) returnera sånger, nil 

Detta är ett utmärkt exempel på att använda en verklig relationell DB som sqlite för implementering av minnesdatabutiken vs att rulla egna, vilket skulle kräva att kartor hålls och att alla bokföringar är korrekta. 

Runningtest mot SQLite

Nu när vi har ett korrekt minnesdataskikt, låt oss ta en titt på testerna. Jag lade dessa tester i ett separat paket som heter sqlite_test, och jag importerar lokalt det abstrakta datalagret (domänmodellen), det konkreta datalagret (för att skapa minnesdataskiktet) och sånghanteraren (koden som testas). Jag förbereder också två låtar för testerna från den sensationella panamanska konstnären El Chombo!

paketet sqlite_test import ("test". "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 = Song Url: url1, Namn:" Chacaron " var testSong2 = Sång Url: url2, Namn:" El Gato Volador " 

Testmetoder skapar ett nytt lagringslager för data i minnet för att börja från början och kan nu ringa metoder i datalagret för att förbereda testmiljön. När allt är inställt kan de åberopa låthanteringsmetoderna och verifiera senare att datalagret innehåller det förväntade tillståndet.

Till exempel, AddSong_Success () testmetod skapar en användare, lägger till en låt med hjälp av Song Manager AddSong () metod och verifierar att senare ringer GetSongsByUser () returnerar den tillagda låten. Det lägger sedan till en annan låt och verifierar igen.

func TestAddSong_Success (t * test.T) u: = Användare Namn: "Gigi", Email: "[email protected]" dl, err: = NewInMemoryDataLayer () om err! = nil t.Error Misslyckades med att skapa minnesdataskikt ") err = dl.CreateUser (u) om err! = Nil t.Error (" Misslyckades med att skapa användare ") lm, err: = NewSongManager (u, dl) om fel ! = nil t.Error ("NewSongManager () returneras nil") err = lm.AddSong (testSong, nil) om err! = nil t.Error ("AddSong () misslyckades") låtar, err : = dl.GetSongsByUser (u) om err! = nil t.Error ("GetSongsByUser () misslyckades") om len (låtar)! = 1 t.Error ('GetSongsByUser () returnerade inte en låt som förväntat ") om låtar [0]! = testSong t.Error (" Låt sang inte matchar inmatningssång ") // Lägg till en annan låt err = lm.AddSong (testSong2, noll) om err! = nil  t.Error ("AddSong () misslyckades") låtar, err = dl.GetSongsByUser (u) om err! = nil t.Error ("GetSongsByUser () misslyckades") om len (låtar)! = 2 t .Error ('GetSongsByUser () returnerade inte två låtar som förväntat') om låtar [0]! = TestSong t.Error ("Added song matchar inte inmatningssången ") om låtar [1]! = testSong2 t.Error (" Tillagd sång matchar inte inmatningssång ") 

De TestAddSong_Duplicate () testmetod är liknande, men istället för att lägga till en ny låt andra gången lägger den till samma låt, vilket resulterar i ett duplikat låtfel:

 u: = Användare Namn: "Gigi", Email: "[email protected]" dl, err: = NewInMemoryDataLayer () om err! = nil t.Error ("Misslyckades med att skapa minnesdataskikt")  err = dl.CreateUser (u) om err! = nil t.Error ("Misslyckades med att skapa användare") lm, err: = NewSongManager (u, dl) om err! = nil t.Error ("NewSongManager () returnerat 'nil' ") err = lm.AddSong (testSong, noll) om err! = nil t.Error (" AddSong () misslyckades ") låtar, err: = dl.GetSongsByUser ! = nil t.Error ("GetSongsByUser () misslyckades") om len (låtar)! = 1 t.Error ('GetSongsByUser () returnerade inte en låt som förväntat') om låtar [0]! = testSong t.Error ("Added song matchar inte inmatningssång") // Lägg till samma låt igen err = lm.AddSong (testSong, noll) om err == nil t.Error ('AddSong () skulle ha misslyckats med en dubblett sång ") expectedErrorMsg: =" Duplicate song "errorMsg: = Err.Error () om errorMsg! = expectedErrorMsg t.Error ('AddSong () returnerade felmeddelande för dubbelsång

Slutsats

I den här handledningen genomförde vi ett minnesdataskikt baserat på SQLite, befolkade en SQLite-databas i minnet med testdata och använde inlagringslagret för att testa programmet.

I del tre kommer vi att fokusera på testning mot ett lokalt komplext datalager som består av flera datalager (en relations DB och en Redis-cache). Håll dig igång.