Tests d'intégration parallèle Postgresql dans l'application GO


Les tests d'intégration sont l'un des niveaux de la pyramide des tests . Ils nécessitent généralement plus de temps, car en eux, nous ne remplaçons rien par des simulations de composants réels. Pour réduire le temps de tels tests, nous pouvons les exécuter en parallèle. Ici, je vais parler spécifiquement de ces tests pour Postgresql.

Idéalement, chaque test devrait être indépendant, afin qu'ils ne s'influencent pas mutuellement. En d'autres termes, chaque fonction de test a son propre état. C'est un bon signe pour utiliser des tests parallèles. Pour obtenir mon ensemble de données personnelles pour chaque fonction de test, j'ai créé une fonction qui, lors du démarrage d'un test, crée un circuit temporaire, y charge des données et détruit le circuit une fois le test terminé. Chaque schéma créé contient un hachage dans le nom pour éviter les conflits de nom.


Fonction d'assistance


Commençons par une fonction d'aide pour afficher les erreurs dans les tests. J'ai pris les fonctions d'assistance de Ben Johnson (Ben Johnson), ce qui m'a aidé à enregistrer quelques lignes de code et à rendre mes erreurs plus claires et détaillées.

Données de test


Pour exécuter le test d'intégration de la base de données, des données de test doivent être fournies. L'outil de test Go a une bonne prise en charge pour le chargement des données de test à partir de fichiers. Tout d'abord, allez créer des dossiers ignorés appelés "testdata". Deuxièmement, lorsque vous exécutez «go test», il change le dossier actuel en dossier de package. Cela vous permet d'utiliser le chemin d'accès relatif au dossier testdata pour charger l'ensemble de données de test.

Création d'une connexion à la base de données pour le 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()) } } } 


Appel de «CreateTestDatabase» pour créer une connexion à la base de données de test et créer un nouveau schéma de données pour les tests. Cette fonction renvoie la connexion à la base de données, le nom du schéma créé et la fonction de purge pour supprimer ce schéma. Pour un test, il vaut mieux échouer le test que renvoyer une erreur à l'appelant. (Remarque: le retour de la fonction de nettoyage est basé sur les tests avancés de Mitchell Hashimoto avec Go Talk ).

Télécharger le jeu de données de test


J'ai utilisé les fichiers «.sql». Un (1) sql contient des données pour une (1) table. Cela inclut la création d'une table et son remplissage avec des données. Tous les fichiers sql sont stockés dans le dossier «testdata». Voici un exemple de fichier sql.

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

Et voici la partie complexe. Parce que chaque fonction s'exécute dans son propre schéma de données unique, nous ne pouvons pas simplement exécuter (écrire) une requête dans ces fichiers sql. Nous devons spécifier le schéma avant les noms de table afin de créer une table ou insérer des données dans le schéma temporaire souhaité. Par exemple, le livre CREATE TABLE ... doit être écrit comme CREATE TABLE uniqueschema.book ... et le livre INSERT INTO ... doit être changé en INSERT INTO uniqueschema.book .... J'ai utilisé des expressions régulières pour modifier les requêtes avant de les exécuter. Voici le code de téléchargement des données de test:

 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() } } 


Création de test


Avant de commencer chaque test, une base de données de test sera créée avec un nom unique pour le schéma et l'exécution de la fonction de nettoyage sera retardée pour supprimer ce schéma. Le nom du schéma sera inséré dans la demande du test. La chose la plus importante dans cette implémentation est que la connexion à la base de données doit être personnalisable pour changer la connexion de la base de données réelle à la connexion à la base de données de test. Ajoutez «t.Parallel ()» au début de chaque fonction de test pour indiquer à l'environnement de test la nécessité d'exécuter ce test en parallèle.
Voici le code complet:

 //            "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)) } 


Remarque: Sous «TestGetBooks», je suppose que la requête renverra 2 livres, comme J'ai donné tellement de données de test dans «testdata / book.sql» bien qu'il y ait un test d'insertion ci-dessus. Si nous ne partageons pas le circuit entre les deux tests, «TestGetBooks» échouera, car maintenant 3 lignes dans le tableau, 2 du test, 1 de l'insert de test ci-dessus. C'est l'avantage de circuits séparés pour les tests - leurs données sont indépendantes, et donc les tests sont indépendants les uns des autres.

L'exemple de projet que j'ai posté ici github . Vous pouvez le copier pour vous-même, exécuter le test et voir le résultat.

Conclusion


Pour mon projet, cette approche réduit le temps de test de 40 à 50% par rapport aux tests séquentiels. Un autre avantage de l'exécution de tests en parallèle est que nous pouvons éviter certaines erreurs qui peuvent se produire lorsqu'une application traite plusieurs actions concurrentielles.

Bon test.

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

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


All Articles