Testen des datenintensiven Codes mit Go, Teil 2

Überblick

Dies ist Teil zwei von fünf in einer Lernreihe zum Testen von datenintensivem Code. Im ersten Teil ging es um das Design einer abstrakten Datenschicht, die ein ordnungsgemäßes Testen ermöglicht, wie mit Fehlern in der Datenschicht umgegangen wird, wie der Datenzugriffscode gemobbt wird und wie mit einer abstrakten Datenschicht getestet wird. In diesem Lernprogramm werde ich einen Vergleich mit einer echten In-Memory-Datenschicht durchführen, die auf der beliebten SQLite basiert. 

Testen gegen einen In-Memory-Datenspeicher

Das Testen mit einer abstrakten Datenschicht ist für einige Anwendungsfälle von Vorteil, in denen Sie sehr genau arbeiten müssen. Sie wissen genau, welche Aufrufe der geprüfte Code für die Datenschicht verwendet, und Sie können die Mock-Antworten vorbereiten.

Manchmal ist es nicht so einfach. Die Reihe von Aufrufen an die Datenschicht ist möglicherweise schwer zu bestimmen oder es ist viel Aufwand erforderlich, um geeignete, gültige Antworten aus der Dose vorzubereiten. In diesen Fällen müssen Sie möglicherweise mit einem In-Memory-Datenspeicher arbeiten. 

Die Vorteile eines In-Memory-Datenspeichers sind:

  • Es ist sehr schnell. 
  • Sie arbeiten gegen einen tatsächlichen Datenspeicher.
  • Sie können es häufig mit Dateien oder Code von Grund auf neu füllen.

Insbesondere wenn Ihr Datenspeicher eine relationale Datenbank ist, ist SQLite eine fantastische Option. Denken Sie daran, dass es Unterschiede zwischen SQLite und anderen gängigen relationalen DBs wie MySQL und PostgreSQL gibt.

Stellen Sie sicher, dass Sie dies in Ihren Tests berücksichtigen. Beachten Sie, dass Sie weiterhin über die abstrakte Datenschicht auf Ihre Daten zugreifen, aber jetzt ist der Sicherungsspeicher während der Tests der In-Memory-Datenspeicher. Ihr Test füllt die Testdaten unterschiedlich aus, der getestete Code weiß jedoch nicht, was vor sich geht.

SQLite verwenden

SQLite ist eine eingebettete Datenbank (verbunden mit Ihrer Anwendung). Es wird kein separater DB-Server ausgeführt. In der Regel werden die Daten in einer Datei gespeichert, es besteht jedoch auch die Möglichkeit eines Sicherungsspeichers im Arbeitsspeicher. 

Hier ist der InMemoryDataStore struct. Es ist auch Teil der concrete_data_layer Dieses Paket importiert das Drittanbieterpaket "go-sqlite3", das die Golang-Standardschnittstelle "database / sql" implementiert.  

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

Erstellen der In-Memory-Datenschicht

Das NewInMemoryDataLayer () Die Konstruktorfunktion erstellt einen im Speicher befindlichen SQL-DB und gibt einen Zeiger auf den zurück InMemoryDataLayer

func NewInMemoryDataLayer () (* InMemoryDataLayer, Fehler) db, err: = sql.Open ("sqlite3", ": memory:") Wenn err! = nil return nil, err err = createSqliteSchema (db) Rückgabe db, nil 

Beachten Sie, dass Sie jedes Mal, wenn Sie eine neue Datenbank ": memory:" öffnen, von vorne anfangen. Wenn Sie für mehrere Anrufe persistieren möchten NewInMemoryDataLayer (), du solltest benutzen file :: memory:? cache = shared. Weitere Informationen finden Sie in diesem GitHub-Diskussionsthread.

Das InMemoryDataLayer implementiert die DataLayer Schnittstelle und speichert die Daten tatsächlich mit korrekten Beziehungen in ihrer SQLite-Datenbank. Dazu müssen wir zunächst ein korrektes Schema erstellen, das genau die Aufgabe des ist createSqliteSchema () Funktion im Konstruktor. Es werden drei Datentabellen (Song, Benutzer, Label und zwei Querverweistabellen) erstellt, label_song und user_song.

Es fügt einige Einschränkungen, Indizes und Fremdschlüssel hinzu, um die Tabellen miteinander in Beziehung zu setzen. Ich werde nicht auf die spezifischen Details eingehen. Das Wichtigste dabei ist, dass die gesamte Schema-DDL als eine einzige Zeichenfolge (bestehend aus mehreren DDL-Anweisungen) deklariert ist, die dann mit der Anweisung ausgeführt wird db.Exec () Methode, und wenn etwas schief geht, gibt es einen Fehler. 

func createSqliteSchema (db * sql.DB) error schema: = 'CREATE TABLE WENN NICHT EXISTEN (ID INTEGER PRIMARY KEY AUTOINCREMENT, URL TEXT UNIQUE, Name TEXT, Beschreibung TEXT); CREATE TABLE WENN NICHT EXISTS-Benutzer (ID INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT, E-Mail TEXT UNIQUE, Registered_at TIMESTAMP, Last_login TIMESTAMP); CREATE INDEX user_email_idx ON Benutzer (E-Mail); CREATE TABLE WENN NOT EXISTS label (ID INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT UNIQUE); CREATE INDEX label_name_idx ON label (name); CREATE TABLE WENN NOT EXISTS label_song (label_id INTEGER NICHT NULL REFERENCES label (id), song_id INTEGER NICHT NULL REFERENCES song (id), PRIMARY KEY (label_id, song_id)); CREATE TABLE WENN NICHT EXISTS ist user_song (user_id INTEGER NICHT NULL REFERENCES user (id), song_id INTEGER NOT NULL REFERENCES song (id), PRIMARY KEY (user_id, song_id)); ' _, err: = db.Exec (Schema), err 

Es ist wichtig zu wissen, dass SQL zwar Standard ist, aber jedes Datenbankverwaltungssystem (DBMS) seine eigene Ausprägung hat und dass die genaue Schemadefinition nicht unbedingt wie bei einer anderen Datenbank funktioniert.

Implementierung der In-Memory-Datenschicht

Um Ihnen einen Eindruck des Implementierungsaufwands einer In-Memory-Datenschicht zu vermitteln, können Sie die folgenden Methoden verwenden: AddSong () und GetSongsByUser ()

Das AddSong () Methode macht viel Arbeit. Es fügt eine Aufzeichnung in das ein Lied Tabelle sowie in jede der Referenztabellen: label_song und user_song. Wenn eine Operation fehlschlägt, wird zu jedem Zeitpunkt nur ein Fehler zurückgegeben. Ich verwende keine Transaktionen, da sie nur für Testzwecke vorgesehen sind und ich mich nicht um Teildaten in der Datenbank kümmern muss.

func (m * InMemoryDataLayer) AddSong (Benutzer Benutzer, Song Song, Labels [] Label) Fehler s: = 'INSERT INTO Song (URL, Name, Beschreibung) Werte (?,?,?)' Anweisung, err: = m .db.Prepare (s) if err! = nil return err Ergebnis, err: = statement.Exec (song.Url, song.Name, song.Description) wenn err! = nil return err songId, err: = result.LastInsertId () if err! = nil return err s = "SELECT id FROM Benutzer, bei dem email =?" Zeilen, err: = m.db.Query (s, user.Email) wenn err! = nil return err var userId int für Zeilen.Next () err = rows.can (& userId) wenn err! = nil  return err s = 'INSERT INTO user_song (user_id, song_id) Werte (?,?)' Anweisung, err = m.db.Prepare (s), wenn err! = nil return err _, err = Anweisung.Exec (userId, songId) if err! = nil return err var labelId int64 s: = "INSERT INTO label (name) Werte (?)" label_ins, err: = m.db.Prepare (s), wenn err! = nil return err s = 'INSERT INTO label_song (label_id, song_id) Werte (?,?)' label_song_ins, err: = m.db.Prepare (s), wenn err! = nil return err für _, t: = Bereichsbeschriftungen s = "SELECT ID FROM Beschriftung wobei Name =?" Zeilen, err: = m.db.Query (s, t.Name), wenn err! = nil return err labelId = -1 für Zeilen.Next () err = Zeilen. Scan (& labelId), wenn err! = nil return err wenn labelId == -1 result, err = label_ins.Exec (t.Name) wenn err! = nil return err labelId, err = result.LastInsertId () wenn err! = nil return err  result, err = label_song_ins.Exec (labelId, songId) wenn err! = nil return err return nil 

Das GetSongsByUser () verwendet eine Verknüpfung + Unterauswahl aus der user_song Querverweis, um Songs für einen bestimmten Benutzer zurückzugeben. Es verwendet die Abfrage() Methoden und scannt dann später jede Zeile, um a zu füllen Lied struct aus dem Domänenobjektmodell und geben ein Stück der Lieder zurück. Die Low-Level-Implementierung als relationale Datenbank ist sicher versteckt.

func (m * InMemoryDataLayer) GetSongsByUser (u Benutzer) ([] Song, Fehler) s: = 'SELECT-URL, Titel, Beschreibung FROM song L INNER JOIN user_song UL ON UL.song_id = L.id WHERE UL.user_id = ( SELECT ID von Benutzer WHERE email =?) 'Zeilen, err: = m.db.Query (s, u.Email), wenn err! = Nil return nil, err für Zeilen.Next () var song Song err = Zeilen.Scan (& song.Url, & song.Title, & song.Description) wenn err! = nil return nil, err Lieder = Anhängen (Titel, Lied) Lieder zurückgeben, nil 

Dies ist ein hervorragendes Beispiel für die Verwendung einer relationalen relationalen Datenbank wie sqlite für die Implementierung des In-Memory-Datenspeichers gegenüber dem Rolling unseres eigenen. Hierfür müssten Karten aufbewahrt und sichergestellt werden, dass die gesamte Buchhaltung korrekt ist. 

Tests gegen SQLite ausführen

Nun, da wir über eine geeignete In-Memory-Datenschicht verfügen, wollen wir uns die Tests ansehen. Ich habe diese Tests in einem separaten Paket namens platziert sqlite_test, und ich importiere lokal die abstrakte Datenschicht (das Domänenmodell), die konkrete Datenschicht (um die In-Memory-Datenschicht zu erstellen) und den Song-Manager (den getesteten Code). Ich bereite auch zwei Songs für die Tests des aufsehenerregenden panamaischen Künstlers El Chombo vor!

Paket 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 = Lied Url: url1, Name:" Chacaron " var testSong2 = Lied Url: url2, Name:" El Gato Volador " 

Testmethoden erstellen eine neue In-Memory-Datenschicht, die bei Null beginnt. Sie können jetzt Methoden auf der Datenschicht aufrufen, um die Testumgebung vorzubereiten. Wenn alles eingerichtet ist, können sie die Song-Manager-Methoden aufrufen und später überprüfen, ob die Datenschicht den erwarteten Status enthält.

Zum Beispiel die AddSong_Success () test method erstellt einen Benutzer und fügt einen Song unter Verwendung des Song-Managers hinzu AddSong () Methode und verifiziert das späteres Aufrufen GetSongsByUser () gibt den hinzugefügten Song zurück. Es fügt dann ein anderes Lied hinzu und überprüft es erneut.

func TestAddSong_Success (t * testing.T) u: = Benutzer Name: "Gigi", E-Mail: "[email protected]" dl, err: = NewInMemoryDataLayer (), wenn err! = nil t.Error (" In-Memory-Datenschicht konnte nicht erstellt werden ") err = dl.CreateUser (u) wenn err! = Nil t.Error (" Benutzer konnte nicht erstellt werden ") lm, err: = NewSongManager (u, dl), wenn ein Fehler auftritt ! = nil t.Error ("NewSongManager () hat 'nil' zurückgegeben" ") err = lm.AddSong (testSong, nil) wenn err! = nil t.Error (" AddSong () failed ") lieder, err : = dl.GetSongsByUser (u) if err! = nil t.Error ("GetSongsByUser () failed") wenn len (Lieder)! = 1 t.Error ('GetSongsByUser () hat keinen Song zurückgegeben erwartet ') wenn Songs [0]! = testSong t.Error ("Hinzugefügter Song passt nicht zum eingegebenen Song") // Fügen Sie einen weiteren Song hinzu: err = lm.AddSong (testSong2, nil), wenn err! = nil  t.Error ("AddSong () failed") lieder, err = dl.GetSongsByUser (u) wenn err! = nil t.Error ("GetSongsByUser () failed") wenn len (songs)! = 2 t .Error ('GetSongsByUser () hat nicht wie erwartet zwei Songs zurückgegeben') wenn Songs [0]! = TestSong t.Error ("Hinzugefügter Song stimmt nicht mit dem eingegebenen Song überein ") wenn Songs [1]! = testSong2 t.Error (" Hinzugefügter Song passt nicht zum eingegebenen Song ") 

Das TestAddSong_Duplicate () Die Testmethode ist ähnlich, aber anstatt ein neues Lied ein zweites Mal hinzuzufügen, fügt es das gleiche Lied hinzu, was zu einem doppelten Liedfehler führt:

 u: = Benutzer Name: "Gigi", E-Mail: "[email protected]" dl, err: = NewInMemoryDataLayer () if err! = nil t.Error ("Fehler beim Erstellen der In-Memory-Datenschicht")  err = dl.CreateUser (u) if err! = nil t.Error ("Benutzer konnte nicht erstellt werden") lm, err: = NewSongManager (u, dl) wenn err! = nil t.Error ("NewSongManager () zurückgegeben 'nil' ") err = lm.AddSong (testSong, nil) if err! = nil t.Error (" AddSong () failed ") lieder, err: = dl.GetSongsByUser (u) if err ! = nil t.Error ("GetSongsByUser () fehlgeschlagen") wenn len (songs)! = 1 t.Error ('GetSongsByUser () hat keinen Song wie erwartet zurückgegeben') wenn songs [0]! = testSong t.Error ("Hinzugefügter Song stimmt nicht mit dem eingegebenen Song überein") // Fügt den gleichen Song erneut hinzu err = lm.AddSong (testSong, nil), wenn err == nil t.Error ('AddSong () sollte für ein Duplikat des Songs fehlgeschlagen sein ") expectedErrorMsg: =" Duplicate Song "errorMsg: = err.Error () if errorMsg! = expectedErrorMsg t.Error ('AddSong () hat eine falsche Fehlermeldung für das Duplikat eines Songs')

Fazit

In diesem Lernprogramm wurde eine auf SQLite basierende In-Memory-Datenschicht implementiert, eine In-Memory-SQLite-Datenbank mit Testdaten gefüllt und die In-Memory-Datenschicht zum Testen der Anwendung verwendet.

In Teil drei konzentrieren wir uns auf das Testen gegen eine lokale komplexe Datenschicht, die aus mehreren Datenspeichern besteht (relationaler DB und Redis-Cache). Bleib dran.