Postgresql Parallel Integration Tests in GO-Anwendung


Integrationstests sind eine der Ebenen der Testpyramide . Normalerweise benötigen sie mehr Zeit, weil In ihnen ersetzen wir nichts durch Simulationen realer Komponenten. Um die Zeit für solche Tests zu verkürzen, können wir sie parallel ausführen. Hier werde ich speziell über solche Tests für Postgresql sprechen.

Im Idealfall sollte jeder Test unabhängig sein, damit sie sich nicht gegenseitig beeinflussen. Mit anderen Worten, jede Testfunktion hat ihren eigenen Zustand. Dies ist ein gutes Zeichen für parallele Tests. Um meinen persönlichen Datensatz für jede Testfunktion abzurufen, habe ich eine Funktion erstellt, die beim Starten eines Tests eine temporäre Schaltung erstellt, Daten in diese lädt und die Schaltung nach Abschluss des Tests zerstört. Jedes erstellte Schema enthält einen Hash im Namen, um Namenskonflikte zu vermeiden.


Hilfsfunktion


Beginnen wir mit einer Hilfsfunktion , um Fehler in den Tests anzuzeigen. Ich habe die Hilfsfunktionen von Ben Johnson (Ben Johnson) übernommen, die mir geholfen haben, ein paar Codezeilen zu speichern und meine Fehler klarer und detaillierter zu machen.

Testdaten


Um den Integrationstest der Datenbank auszuführen, müssen Testdaten bereitgestellt werden. Das Go-Testtool unterstützt das Laden von Testdaten aus Dateien. Erstellen Sie zunächst überspringende Ordner mit dem Namen "Testdaten". Zweitens ändert sich beim Ausführen von "go test" der aktuelle Ordner in den Paketordner. Auf diese Weise können Sie den relativen Pfad zum Ordner testdata verwenden, um den Testdatensatz zu laden.

Erstellen einer Datenbankverbindung für den Test


package database import ( "math/rand" "strconv" "testing" "time" _ "github.com/lib/pq" "database/sql" ) const ( dbPort = 5439 dbUser = "postgres" dbPassword = "postgres" dbName = "test" ) func CreateTestDatabase(t *testing.T) (*sql.DB, string, func()) { connectionString := fmt.Sprintf("port=%d user=%s password=%s dbname=%s sslmode=disable", dbPort, dbUser, dbPassword, dbName) db, dbErr := sql.Open("postgres", connectionString) if dbErr != nil { t.Fatalf("Fail to create database. %s", dbErr.Error()) } rand.Seed(time.Now().UnixNano()) schemaName := "test" + strconv.FormatInt(rand.Int63(), 10) _, err := db.Exec("CREATE SCHEMA " + schemaName) if err != nil { t.Fatalf("Fail to create schema. %s", err.Error()) } return db, schemaName, func() { _, err := db.Exec("DROP SCHEMA " + schemaName + " CASCADE") if err != nil { t.Fatalf("Fail to drop database. %s", err.Error()) } } } 


Rufen Sie "CreateTestDatabase" auf, um eine Verbindung zur Testdatenbank herzustellen und ein neues Datenschema für die Tests zu erstellen. Diese Funktion gibt die Datenbankverbindung, den Namen des erstellten Schemas und die Bereinigungsfunktion zum Löschen dieses Schemas zurück. Für einen Test ist es besser, den Test nicht zu bestehen, als einen Fehler an den Anrufer zurückzugeben. (Hinweis: Die Rückgabe der Bereinigungsfunktion basiert auf Mitchell Hashimotos Advanced Testing with Go Talk. )

Laden Sie den Testdatensatz herunter


Ich habe die ".sql" -Dateien verwendet. Eine (1) SQL enthält Daten für eine (1) Tabelle. Dazu gehört das Erstellen einer Tabelle und das Auffüllen mit Daten. Alle SQL-Dateien werden im Ordner "testdata" gespeichert. Hier ist eine Beispiel-SQL-Datei.

 CREATE TABLE book ( title character varying(50), author character varying(50) ); INSERT INTO book VALUES ('First Book','First Author'), ('Second Book','Second Author') ; 

Und hier ist der komplizierte Teil. Da jede Funktion in einem eigenen Datenschema ausgeführt wird, können wir in diesen SQL-Dateien nicht einfach eine Abfrage ausführen (schreiben). Wir müssen das Schema vor den Tabellennamen angeben, um eine Tabelle zu erstellen oder Daten in das gewünschte temporäre Schema einzufügen. Zum Beispiel sollte ein CREATE TABLE-Buch ... als CREATE TABLE uniqueschema.book geschrieben werden ... und ein INSERT INTO-Buch ... muss in INSERT INTO uniqueschema.book geändert werden .... Ich habe reguläre Ausdrücke verwendet, um Abfragen vor der Ausführung zu ändern. Hier ist der Download-Code für die Testdaten:

 package datalayer import ( "bufio" "fmt" "io" "os" "regexp" "testing" "database/sql" "github.com/Hendra-Huang/databaseintegrationtest/testingutil" //     (   ,  79) ) //        var schemaPrefixRegexps = [...]*regexp.Regexp{ regexp.MustCompile(`(?i)(^CREATE SEQUENCE\s)(["\w]+)(.*)`), regexp.MustCompile(`(?i)(^CREATE TABLE\s)(["\w]+)(\s.+)`), regexp.MustCompile(`(?i)(^ALTER TABLE\s)(["\w]+)(\s.+)`), regexp.MustCompile(`(?i)(^UPDATE\s)(["\w]+)(\s.+)`), regexp.MustCompile(`(?i)(^INSERT INTO\s)(["\w]+)(\s.+)`), regexp.MustCompile(`(?i)(^DELETE FROM\s)(["\w]+)(.*)`), regexp.MustCompile(`(?i)(.+\sFROM\s)(["\w]+)(.*)`), regexp.MustCompile(`(?i)(\sJOIN\s)(["\w]+)(.*)`), } //      func addSchemaPrefix(schemaName, query string) string { prefixedQuery := query for _, re := range schemaPrefixRegexps { prefixedQuery = re.ReplaceAllString(prefixedQuery, fmt.Sprintf("${1}%s.${2}${3}", schemaName)) } return prefixedQuery } func loadTestData(t *testing.T, db *sql.DB, schemaName string, testDataNames ...string) { for _, testDataName := range testDataNames { file, err := os.Open(fmt.Sprintf("./testdata/%s.sql", testDataName)) testingutil.Ok(t, err) reader := bufio.NewReader(file) var query string for { line, err := reader.ReadString('\n') if err == io.EOF { break } testingutil.Ok(t, err) line = line[:len(line)-1] if line == "" { query = addSchemaPrefix(schemaName, query) _, err := db.Exec(query) testingutil.Ok(t, err) query = "" } query += line } file.Close() } } 


Testerstellung


Vor dem Start jedes Tests wird eine Testdatenbank mit einem eindeutigen Namen für das Schema erstellt und die Ausführung der Bereinigungsfunktion zum Löschen dieses Schemas wird verschoben. Der Schemaname wird im Test in die Anforderung eingefügt. Das Wichtigste bei dieser Implementierung ist, dass die Verbindung zur Datenbank anpassbar sein muss, um die Verbindung von der realen Datenbank zur Verbindung zur Testdatenbank zu ändern. Fügen Sie zu Beginn jeder Testfunktion "t.Parallel ()" hinzu, um der Testumgebung anzuzeigen, dass dieser Test parallel ausgeführt werden muss.
Unten ist der vollständige Code:

 //            "integration" (. build flags) // +build integration package datalayer import ( "context" "testing" "github.com/Hendra-Huang/databaseintegrationtest/database" "github.com/Hendra-Huang/databaseintegrationtest/testingutil" ) func TestInsertBook(t *testing.T) { t.Parallel() db, schemaName, cleanup := database.CreateTestDatabase(t) defer cleanup() loadTestData(t, db, schemaName, "book") // will load data which the filename is book title := "New title" author := "New author" // those 2 lines code below are not a good practice // but it is intentional to keep the focus only on integration test part // the important part is database connection has to be configurable insertBookQuery = addSchemaPrefix(schemaName, insertBookQuery) // override the query and add schema to the query err := InsertBook(context.Background(), db, title, author) // will execute insertBookQuery with the provided connection testingutil.Ok(t, err) } func TestGetBooks(t *testing.T) { t.Parallel() db, schemaName, cleanup := database.CreateTestDatabase(t) defer cleanup() loadTestData(t, db, schemaName, "book") getBooksQuery = addSchemaPrefix(schemaName, getBooksQuery) books, err := GetBooks(context.Background(), db) testingutil.Ok(t, err) testingutil.Equals(t, 2, len(books)) } 


Hinweis: Unter "TestGetBooks" gehe ich davon aus, dass die Abfrage 2 Bücher als zurückgibt Ich habe so viel Testdatensatz in "testdata / book.sql" angegeben, obwohl es oben einen Insert-Test gibt. Wenn wir die Schaltung zwischen den beiden Tests nicht teilen, schlägt "TestGetBooks" fehl, weil jetzt 3 Zeilen in der Tabelle, 2 aus dem Test, 1 aus dem Testeinsatz oben. Dies ist der Vorteil von getrennten Schaltkreisen für Tests - ihre Daten sind unabhängig und daher sind die Tests unabhängig voneinander.

Das Projektbeispiel habe ich hier github gepostet. Sie können es in sich selbst kopieren, den Test ausführen und das Ergebnis sehen.

Fazit


Bei meinem Projekt reduziert dieser Ansatz die Testzeit im Vergleich zu sequentiellen Tests um 40–50%. Ein weiterer Vorteil der parallelen Ausführung von Tests besteht darin, dass wir einige Fehler vermeiden können, die auftreten können, wenn eine Anwendung mehrere Wettbewerbsaktionen verarbeitet.

Hab einen schönen Test.

- Bild von medium.com/kongkow-it-medan/parallel-database-integration-test-on-go-application-8706b150ee2e

Source: https://habr.com/ru/post/de466459/


All Articles