Gegevens-intensieve code testen met Go, deel 2

Overzicht

Dit is deel twee van de vijf in een zelfstudieserie over het testen van gegevensintensieve code. In het eerste deel behandelde ik het ontwerp van een abstracte gegevenslaag die goed testen mogelijk maakt, hoe om te gaan met fouten in de gegevenslaag, hoe de toegangscode voor gegevens te bespotten en hoe te testen tegen een abstracte gegevenslaag. In deze zelfstudie zal ik gaan testen met een echte in-memory gegevenslaag op basis van de populaire SQLite. 

Testen tegen een In-Memory Data Store

Testen met een abstracte gegevenslaag is geweldig voor sommige gevallen waarin u veel precisie nodig heeft, u begrijpt precies wat oproepen die de te testen code gaat maken ten opzichte van de gegevenslaag en u bent goed met het voorbereiden van de schijnreacties.

Soms is het niet zo eenvoudig. De reeks oproepen naar de gegevenslaag kan moeilijk zijn om te achterhalen, of het kost veel moeite om juiste ingeblikte responsen voor te bereiden die geldig zijn. In deze gevallen moet u wellicht tegen een in-memory datastore werken. 

De voordelen van een in-memory data store zijn:

  • Het is erg snel. 
  • Je werkt tegen een echte data store.
  • U kunt het vaak helemaal opnieuw vullen met behulp van bestanden of code.

In het bijzonder als uw gegevensarchief een relationele DB is, is SQLite een fantastische optie. Vergeet niet dat er verschillen zijn tussen SQLite en andere populaire relationele DB's zoals MySQL en PostgreSQL.

Zorg ervoor dat u daar rekening mee houdt bij uw tests. Merk op dat je nog steeds toegang hebt tot je gegevens via de abstracte gegevenslaag, maar nu is de back-upopslag tijdens tests de gegevensopslag in het geheugen. Uw test voert de testgegevens anders in, maar de code die wordt getest is zalig niet op de hoogte van wat er aan de hand is.

SQLite gebruiken

SQLite is een ingesloten DB (gekoppeld aan uw toepassing). Er is geen afzonderlijke DB-server actief. Het slaat meestal de gegevens op in een bestand, maar heeft ook de mogelijkheid van een back-up in het geheugen. 

Hier is de InMemoryDataStore struct. Het maakt ook deel uit van de concrete_data_layer pakket en importeert het go-sqlite3 pakket van derden dat de standaard Golang "database / sql" -interface implementeert.  

package concrete_data_layer import ("database / sql". "abstract_data_layer" _ "github.com/mattn/go-sqlite3" "time" "fmt") type InMemoryDataLayer struct db * sql.DB

Het construeren van de In-Memory-gegevenslaag

De NewInMemoryDataLayer () constructorfunctie maakt een sqlite DB in het geheugen en retourneert een pointer naar de InMemoryDataLayer

func NewInMemoryDataLayer () (* InMemoryDataLayer, error) db, err: = sql.Open ("sqlite3", ": memory:") if err! = nil return nil, err err = createSqliteSchema (db) return & InMemoryDataLayer  db, nihil 

Merk op dat elke keer dat u een nieuwe ": memory:" DB opent, u helemaal opnieuw begint. Als u persistentie over meerdere oproepen wilt hebben NewInMemoryDataLayer (), je zou ... moeten gebruiken bestand :: geheugen:? cache = gedeeld. Zie deze GitHub discussiethread voor meer details.

De InMemoryDataLayer implementeert de dataLayer interface en slaat de gegevens daadwerkelijk op met de juiste relaties in de sqlite-database. Om dat te doen, moeten we eerst een goed schema maken, wat precies de taak van de is createSqliteSchema () functie in de constructor. Er worden drie gegevenstabellen gemaakt (nummer, gebruiker en label) en twee tabellen met kruisverwijzingen, label_song en user_song.

Het voegt een aantal beperkingen, indexen en externe sleutels toe om de tabellen aan elkaar te relateren. Ik zal niet stilstaan ​​bij de specifieke details. De kern hiervan is dat de gehele DDL van het schema wordt gedeclareerd als een enkele reeks (bestaande uit meerdere DDL-instructies) die vervolgens worden uitgevoerd met behulp van de db.Exec () methode en als er iets fout gaat, wordt er een fout geretourneerd. 

func createSqliteSchema (db * sql.DB) error schema: = 'MAAK TABLE INDIEN NIET BEGONNEN song (id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, naam TEXT, omschrijving TEXT); CREËER TAFEL ALS NIET BESTAAT gebruiker (id INTEGER PRIMAIRE KEY AUTOINCREMENT, naam TEXT, e-mail TEXT UNIQUE, registered_at TIMESTAMP, last_login TIMESTAMP); CREATE INDEX gebruiker_email_idx AAN gebruiker (e-mail); CREËER TAFEL ALS NIET BESTAAT label (id INTEGER PRIMARY KEY AUTOINCREMENT, naam TEXT UNIQUE); CREATE INDEX label_name_idx ON-label (naam); CREËER TAFEL ALS NIET BESTAAT label_song (label_id INTEGER NOT NULL REFERENCES label (id), song_id INTEGER NOT NULL REFERENCES song (id), PRIMARY KEY (label_id, song_id)); CREATEER DE TABEL INDIEN NIET BESTAAT user_song (user_id INTEGER NOT NULL REFERENCES user (id), song_id INTEGER NOT NULL REFERENCES song (id), PRIMARY KEY (user_id, song_id)); ' _, err: = db.Exec (schema) retourfout 

Het is belangrijk om te beseffen dat SQL standaard is, maar dat elk databasebeheersysteem (DBMS) een eigen smaak heeft en dat de exacte schemadefinitie niet noodzakelijkerwijs werkt voor een andere DB.

Implementatie van de In-Memory Data Layer

Om u een idee te geven van de implementatie-inspanning van een in-memory gegevenslaag, volgen hier een aantal methoden: AddSong () en GetSongsByUser ()

De AddSong () methode doet veel werk. Het voegt een record in de lied tabel en in elk van de referentietabellen: label_song en user_song. Op elk punt, als een bewerking mislukt, retourneert het gewoon een fout. Ik gebruik geen transacties omdat deze alleen voor testdoeleinden zijn ontworpen en ik maak me geen zorgen over gedeeltelijke gegevens in de database.

func (m * InMemoryDataLayer) AddSong (gebruiker User, song Song, labels [] Label) error s: = 'INSERT INTO song (url, naam, beschrijving) waarden (?,?,?)' statement, err: = m .db.Prepare (s) if err! = nil return err result, err: = statement.Exec (song.Url, song.Name, song.Description) if err! = nil return err songId, err: = result.LastInsertId () if err! = nil return err s = "SELECT ID FROM user where email =?" rows, err: = m.db.Query (s, user.Email) if err! = nil return err var userId int voor rows.Next () err = rows.Scan (& userId) if err! = nil  return err s = 'INSERT INTO user_song (user_id, song_id) waarden (?,?)' statement, err = m.db.Prepare (s) if err! = nil return err _, err = statement.Exec (userId, songId) if err! = nil return err var labelId int64 s: = "INSERT INTO label (naam) waarden (?)" label_ins, err: = m.db.Prepare (s) if err! = nil return err s = 'INSERT IN label_song (label_id, song_id) waarden (?,?)' label_song_ins, err: = m.db.Prepare (s) if err! = nil return err voor _, t: = bereik labels s = "SELECT ID FROM label waar naam =?" rows, err: = m.db.Query (s, t.Name) if err! = nil return err labelId = -1 voor rows.Next () err = rows.Scan (& labelId) if err! = nil return err als labelId == -1 result, err = label_ins.Exec (t.Name) if err! = nil return err labelId, err = result.LastInsertId () if err! = nil return err  resultaat, err = label_song_ins.Exec (labelId, songId) if err! = nil return err return nil 

De GetSongsByUser () gebruikt een join + sub-selectie uit de user_song kruisverwijzing om nummers terug te sturen voor een specifieke gebruiker. Het gebruikt de Query () methoden en vervolgens scant elke rij om a te vullen lied struct van het domein-objectmodel en retourneer een segment met nummers. De low-level implementatie als een relationele database is veilig verborgen.

func (m * InMemoryDataLayer) GetSongsByUser (u Gebruiker) ([] Song, error) s: = 'SELECT url, titel, beschrijving FROM song L INNERLIJK JOIN user_song UL ON UL.song_id = L.id WHEEuser_id = ( SELECT ID van gebruiker WHERE email =?) 'Rows, err: = m.db.Query (s, u.Email) if err! = Nil return nil, err voor rows.Next () var song Song err = rows.Scan (& song.Url, & song.Title, & song.Description) if err! = nil return nil, err songs = append (songs, song) return songs, nil 

Dit is een geweldig voorbeeld van het gebruik van een echte relationele DB-achtige sqlite voor het implementeren van de in-memory datastore versus het rollen van onze eigen, waarvoor kaarten moeten worden bijgehouden en ervoor moet zorgen dat alle boekhouding correct is. 

Tests uitvoeren tegen SQLite

Nu we een goede gegevenslaag in het geheugen hebben, laten we de tests eens bekijken. Ik plaatste deze tests in een apart pakket met de naam sqlite_test, en ik importeer lokaal de abstracte gegevenslaag (het domeinmodel), de betonnen gegevenslaag (om de gegevenslaag in het geheugen te maken) en de nummerbeheerder (de code die getest wordt). Ik bereid ook twee liedjes voor op de tests van de sensationele Panamese kunstenaar El Chombo!

pakket sqlite_test import ("testen". "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 = Lied url: url1, Naam:" Chacaron " var testSong2 = Lied URL: url2, Naam:" El Gato Volador " 

Testmethoden creëren een nieuwe in-memory-gegevenslaag om helemaal opnieuw te beginnen en kunnen nu methoden op de gegevenslaag aanroepen om de testomgeving voor te bereiden. Wanneer alles is ingesteld, kunnen ze de methoden voor nummerbeheer aanroepen en later controleren of de gegevenslaag de verwachte status bevat.

Bijvoorbeeld de AddSong_Success () testmethode maakt een gebruiker aan, voegt een nummer toe met behulp van de liedbeheerprogramma's AddSong () methode, en verifieert dat later bellen GetSongsByUser () geeft het toegevoegde nummer als resultaat. Vervolgens wordt er nog een nummer toegevoegd en opnieuw gecontroleerd.

func TestAddSong_Success (t * testing.T) u: = Gebruiker Name: "Gigi", Email: "[email protected]" dl, err: = NewInMemoryDataLayer () if err! = nil t.Error (" Kan geen in-memory gegevenslaag maken ") err = dl.CreateUser (u) if err! = Nil t.Error (" Kan gebruiker niet aanmaken ") lm, err: = NewSongManager (u, dl) if err ! = nil t.Error ("NewSongManager ()" nihil "terug") err = lm.AddSong (testSong, nihil) if err! = nil t.Error ("AddSong () failed") songs, err : = dl.GetSongsByUser (u) if err! = nil t.Error ("GetSongsByUser () failed") if len (songs)! = 1 t.Error ('GetSongsByUser () heeft één nummer niet geretourneerd als expected ') if songs [0]! = testSong t.Error ("Toegevoegd nummer komt niet overeen met input liedje") // Voeg een ander nummer toe err = lm.AddSong (testSong2, nil) if err! = nil  t.Error ("AddSong () failed") songs, err = dl.GetSongsByUser (u) if err! = nil t.Error ("GetSongsByUser () failed") if len (songs)! = 2 t .Error ('GetSongsByUser () heeft niet twee nummers geretourneerd zoals verwacht') als liedjes [0]! = TestSong t.Error ("Toegevoegd liedje komt niet overeen met het ingevoerde nummer ") als liedjes [1]! = testSong2 t.Error (" Toegevoegd nummer komt niet overeen met het ingevoerde nummer ") 

De TestAddSong_Duplicate () testmethode is vergelijkbaar, maar in plaats van een nieuwe song de tweede keer toe te voegen, wordt hetzelfde nummer toegevoegd, wat resulteert in een dubbele songfout:

 u: = Gebruiker Name: "Gigi", Email: "[email protected]" dl, err: = NewInMemoryDataLayer () if err! = nil t.Error ("Kan geen in-geheugen gegevenslaag maken")  err = dl.CreateUser (u) if err! = nil t.Error ("Kan gebruiker niet maken") lm, err: = NewSongManager (u, dl) if err! = nil t.Error ("NewSongManager () terug 'nul' ") err = lm.AddSong (testSong, nihil) if err! = nil t.Error (" AddSong () failed ") songs, err: = dl.GetSongsByUser (u) if err ! = nil t.Error ("GetSongsByUser () failed") if len (songs)! = 1 t.Error ('GetSongsByUser () heeft geen enkele song geretourneerd zoals verwacht') als liedjes [0]! = testSong t.Error ("Toegevoegd nummer komt niet overeen met het ingevoerde nummer") // Voeg hetzelfde nummer opnieuw toe err = lm.AddSong (testSong, nil) if err == nil t.Error ('AddSong () had moeten mislukken voor een duplicaat nummer ') expectedErrorMsg: = "Duplicate song" errorMsg: = err.Error () if errorMsg! = expectedErrorMsg t.Error (' AddSong () heeft verkeerd foutbericht voor duplicaatnummer geretourneerd ')

Conclusie

In deze zelfstudie hebben we een gegevenslaag in het geheugen geïmplementeerd op basis van SQLite, een SQLite-database in het geheugen met testgegevens gevuld en de gegevenslaag in het geheugen gebruikt om de toepassing te testen.

In deel drie zullen we ons richten op het testen tegen een lokale complexe gegevenslaag die uit meerdere gegevensarchieven bestaat (een relationele DB en een Redis-cache). Blijf kijken.