


Introduction
Cet article va vous expliquer comment utiliser les tests unitaires et les tests d'intégration pour améliorer votre expérience de développement lorsque vous créez des API de repos dans Golang.
-
Les
Les tests unitaires sont conçus pour vérifier la fonctionnalité des plus petites parties individuelles d'une application, se concentrant souvent sur une seule fonction ou méthode. Ces tests sont effectués indépendamment des autres parties du code pour garantir que chaque composant fonctionne comme prévu seul.
Les Tests d'intégration, quant à eux, évaluent comment les différents modules ou composants de l'application fonctionnent ensemble. Dans cet article, nous nous concentrerons sur les tests d'intégration de notre application Go, en vérifiant spécifiquement qu'elle interagit correctement avec une base de données PostgreSQL en créant et en exécutant avec succès des requêtes SQL.
Cet article suppose que vous êtes familier avec Golang et comment créer une API de repos dans Golang, l'accent principal sera mis sur la création de tests pour vos routes (tests unitaires) et sur le test de vos fonctions de requête SQL (tests d'intégration) pour référence, visitez le github pour jeter un oeil au projet.
Configuration
En supposant que vous ayez configuré votre projet similaire à celui lié ci-dessus, vous aurez une structure de dossiers similaire à celle-ci
test_project |__cmd |__api |__api.go |__main.go |__db |___seed.go |__internal |___db |___db.go |___services |___records |___routes_test.go |___routes.go |___store_test.go |___store.go |___user |___routes_test.go |___routes.go |___store_test.go |___store.go |__test_data |__docker-compose.yml |__Dockerfile |__Makefile
Les tests en Golang sont faciles par rapport à d'autres langages que vous avez peut-être rencontrés grâce au package de tests intégré qui fournit les outils nécessaires pour écrire des tests.
Les fichiers de test sont nommés avec _test.go, ce suffixe permet à go de cibler ces fichiers pour l'exécution lors de l'exécution de la commande go test.
Le point d'entrée de notre projet est le fichier main.go situé dans le dossier cmd
// main.go package main import ( "log" "finance-crud-app/cmd/api" "finance-crud-app/internal/db" "github.com/gorilla/mux" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" ) type Server struct { db *sqlx.DB mux *mux.Router } func NewServer(db *sqlx.DB, mux *mux.Router) *Server { return &Server{ db: db, mux: mux, } } func main() { connStr := "postgres://postgres:Password123@localhost:5432/crud_db?sslmode=disable" dbconn, err := db.NewPGStorage(connStr) if err != nil { log.Fatal(err) } defer dbconn.Close() server := api.NewAPIServer(":8085", dbconn) if err := server.Run(); err != nil { log.Fatal(err) } }
D'après le code que vous pouvez voir, nous créons un nouveau serveur API en transmettant une connexion à la base de données et un numéro de port. Après avoir créé le serveur, nous l'exécutons sur le port indiqué.
La commande NewAPIServer provient du fichier api.go qui
// api.go package api import ( "finance-crud-app/internal/services/records" "finance-crud-app/internal/services/user" "log" "net/http" "github.com/gorilla/mux" "github.com/jmoiron/sqlx" ) type APIServer struct { addr string db *sqlx.DB } func NewAPIServer(addr string, db *sqlx.DB) *APIServer { return &APIServer{ addr: addr, db: db, } } func (s *APIServer) Run() error { router := mux.NewRouter() subrouter := router.PathPrefix("/api/v1").Subrouter() userStore := user.NewStore(s.db) userHandler := user.NewHandler(userStore) userHandler.RegisterRoutes(subrouter) recordsStore := records.NewStore(s.db) recordsHandler := records.NewHandler(recordsStore, userStore) recordsHandler.RegisterRoutes(subrouter) log.Println("Listening on", s.addr) return http.ListenAndServe(s.addr, router) }
Pour cette API, nous utilisons mux comme routeur http.
Test d'intégration
Nous avons une structure User Store qui gère les requêtes SQL liées à l'entité utilisateur.
// store.go package user import ( "errors" "finance-crud-app/internal/types" "fmt" "log" "github.com/jmoiron/sqlx" ) var ( CreateUserError = errors.New("cannot create user") RetrieveUserError = errors.New("cannot retrieve user") DeleteUserError = errors.New("cannot delete user") ) type Store struct { db *sqlx.DB } func NewStore(db *sqlx.DB) *Store { return &Store{db: db} } func (s *Store) CreateUser(user types.User) (user_id int, err error) { query := ` INSERT INTO users (firstName, lastName, email, password) VALUES (, , , ) RETURNING id` var userId int err = s.db.QueryRow(query, user.FirstName, user.LastName, user.Email, user.Password).Scan(&userId) if err != nil { return -1, CreateUserError } return userId, nil } func (s *Store) GetUserByEmail(email string) (types.User, error) { var user types.User err := s.db.Get(&user, "SELECT * FROM users WHERE email = ", email) if err != nil { return types.User{}, RetrieveUserError } if user.ID == 0 { log.Fatalf("user not found") return types.User{}, RetrieveUserError } return user, nil } func (s *Store) GetUserByID(id int) (*types.User, error) { var user types.User err := s.db.Get(&user, "SELECT * FROM users WHERE id = ", id) if err != nil { return nil, RetrieveUserError } if user.ID == 0 { return nil, fmt.Errorf("user not found") } return &user, nil } func (s *Store) DeleteUser(email string) error { user, err := s.GetUserByEmail(email) if err != nil { return DeleteUserError } // delete user records first _, err = s.db.Exec("DELETE FROM records WHERE userid = ", user.ID) if err != nil { return DeleteUserError } _, err = s.db.Exec("DELETE FROM users WHERE email = ", email) if err != nil { return DeleteUserError } return nil }
Dans le fichier ci-dessus, nous avons 3 méthodes de réception de pointeur :
- Créer un utilisateur
- GetUserByEmail
- GetUserById
Pour que ces méthodes remplissent leur fonction, elles doivent interagir avec un système externe qui, dans ce cas, est Postgres DB .
Pour tester cette méthode, nous allons d'abord créer un fichier store_test.go. En go, nous nommons généralement nos fichiers de test d'après le fichier que nous souhaitons tester et ajoutons le suffixe _test.go .
// store_test.go package user_test import ( "finance-crud-app/internal/db" "finance-crud-app/internal/services/user" "finance-crud-app/internal/types" "log" "os" "testing" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" ) var ( userTestStore *user.Store testDB *sqlx.DB ) func TestMain(m *testing.M) { // database ConnStr := "postgres://postgres:Password123@localhost:5432/crud_db?sslmode=disable" testDB, err := db.NewPGStorage(ConnStr) if err != nil { log.Fatalf("could not connect %v", err) } defer testDB.Close() userTestStore = user.NewStore(testDB) code := m.Run() os.Exit(code) } func TestCreateUser(t *testing.T) { test_data := map[string]struct { user types.User result any }{ "should PASS valid user email used": { user: types.User{ FirstName: "testfirsjjlkjt-1", LastName: "testlastkjh-1", Email: "validuser@email.com", Password: "00000000", }, result: nil, }, "should FAIL invalid user email used": { user: types.User{ FirstName: "testFirstName1", LastName: "testLastName1", Email: "test1@email.com", Password: "800890", }, result: user.CreateUserError, }, } for name, tc := range test_data { t.Run(name, func(t *testing.T) { value, got := userTestStore.CreateUser(tc.user) if got != tc.result { t.Errorf("test fail expected %v got %v instead and value %v", tc.result, got, value) } }) } t.Cleanup(func() { err := userTestStore.DeleteUser("validuser@email.com") if err != nil { t.Errorf("could not delete user %v got error %v", "validuser@email.com", err) } }) } func TestGetUserByEmail(t *testing.T) { test_data := map[string]struct { email string result any }{ "should pass valid user email address used": { email: "test1@email.com", result: nil, }, "should fail invalid user email address used": { email: "validuser@email.com", result: user.RetrieveUserError, }, } for name, tc := range test_data { got, err := userTestStore.GetUserByEmail(tc.email) if err != tc.result { t.Errorf("test fail expected %v instead got %v", name, got) } } } func TestGetUserById(t *testing.T) { testUserId, err := userTestStore.CreateUser(types.User{ FirstName: "userbyid", LastName: "userbylast", Email: "unique_email", Password: "unique_password", }) if err != nil { log.Panicf("got %v when creating testuser", testUserId) } test_data := map[string]struct { user_id int result any }{ "should pass valid user id used": { user_id: testUserId, result: nil, }, "should fail invalid user id used": { user_id: 0, result: user.RetrieveUserError, }, } for name, tc := range test_data { t.Run(name, func(t *testing.T) { _, got := userTestStore.GetUserByID(tc.user_id) if got != tc.result { t.Errorf("error retrieving user by id got %v want %v", got, tc.result) } }) } t.Cleanup(func() { err := userTestStore.DeleteUser("unique_email") if err != nil { t.Errorf("could not delete user %v got error %v", "unique_email", err) } }) } func TestDeleteUser(t *testing.T) { testUserId, err := userTestStore.CreateUser(types.User{ FirstName: "userbyid", LastName: "userbylast", Email: "delete_user@email.com", Password: "unique_password", }) if err != nil { log.Panicf("got %v when creating testuser", testUserId) } test_data := map[string]struct { user_email string result error }{ "should pass user email address used": { user_email: "delete_user@email.com", result: nil, }, } for name, tc := range test_data { t.Run(name, func(t *testing.T) { err = userTestStore.DeleteUser(tc.user_email) if err != tc.result { t.Errorf("error deletig user got %v instead of %v", err, tc.result) } }) } t.Cleanup(func() { err := userTestStore.DeleteUser("delete_user@email.com") if err != nil { log.Printf("could not delete user %v got error %v", "delete_user@email.com", err) } }) }
Parcourons le fichier en regardant ce que fait chaque section.
La première action consiste à déclarer les variables userTestStore et testDB. Ces variables seront utilisées pour stocker des pointeurs vers le magasin utilisateur et la base de données respectivement. La raison pour laquelle nous les avons déclarés dans la portée globale du fichier est que nous voulons que toutes les fonctions du fichier de test aient accès aux pointeurs.
La fonction TestMain nous permet d'effectuer quelques actions de configuration avant l'exécution du test principal. Nous nous connectons initialement au magasin postgres et enregistrons le pointeur dans notre variable globale.
Nous avons utilisé ce pointeur pour créer un userTestStore que nous utiliserons pour exécuter les requêtes SQL que nous essayons de connecter.
defer testDB.Close() ferme la connexion à la base de données une fois le test terminé
code := m.Run() exécute le reste de la fonction de test avant de revenir et de quitter.
La fonctionTestCreateUser gérera les tests de la fonction create_user. Notre objectif est de tester si la fonction créera l'utilisateur si un e-mail unique est transmis et la fonction ne devrait pas pouvoir créer un utilisateur si un e-mail non unique a déjà été utilisé pour créer un autre utilisateur.
Nous créons d'abord les données de test que nous utiliserons pour tester les deux scénarios de cas.
test_project |__cmd |__api |__api.go |__main.go |__db |___seed.go |__internal |___db |___db.go |___services |___records |___routes_test.go |___routes.go |___store_test.go |___store.go |___user |___routes_test.go |___routes.go |___store_test.go |___store.go |__test_data |__docker-compose.yml |__Dockerfile |__Makefile
Je vais parcourir la carte en exécutant la fonction create_user avec la date du test comme paramètres et comparer si la valeur renvoyée est la même que le résultat attendu
// main.go package main import ( "log" "finance-crud-app/cmd/api" "finance-crud-app/internal/db" "github.com/gorilla/mux" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" ) type Server struct { db *sqlx.DB mux *mux.Router } func NewServer(db *sqlx.DB, mux *mux.Router) *Server { return &Server{ db: db, mux: mux, } } func main() { connStr := "postgres://postgres:Password123@localhost:5432/crud_db?sslmode=disable" dbconn, err := db.NewPGStorage(connStr) if err != nil { log.Fatal(err) } defer dbconn.Close() server := api.NewAPIServer(":8085", dbconn) if err := server.Run(); err != nil { log.Fatal(err) } }
Dans les cas où le résultat renvoyé n'est pas le même que le résultat attendu, notre test échouera
La dernière partie de cette fonction utilise la fonction Cleanup du package de tests intégré. Cette fonction a enregistré une fonction qui sera appelée lorsque toutes les fonctions du test auront déjà été exécutées. Dans notre exemple de cas, nous utilisons ici la fonction pour effacer les données utilisateur qui ont été utilisées lors de l'exécution de cette fonction de test.
Tests unitaires
Pour nos tests unitaires, nous allons tester les gestionnaires de routes pour notre API. Dans ce cas, les itinéraires liés à l'entité utilisateur. Observez ci-dessous.
// api.go package api import ( "finance-crud-app/internal/services/records" "finance-crud-app/internal/services/user" "log" "net/http" "github.com/gorilla/mux" "github.com/jmoiron/sqlx" ) type APIServer struct { addr string db *sqlx.DB } func NewAPIServer(addr string, db *sqlx.DB) *APIServer { return &APIServer{ addr: addr, db: db, } } func (s *APIServer) Run() error { router := mux.NewRouter() subrouter := router.PathPrefix("/api/v1").Subrouter() userStore := user.NewStore(s.db) userHandler := user.NewHandler(userStore) userHandler.RegisterRoutes(subrouter) recordsStore := records.NewStore(s.db) recordsHandler := records.NewHandler(recordsStore, userStore) recordsHandler.RegisterRoutes(subrouter) log.Println("Listening on", s.addr) return http.ListenAndServe(s.addr, router) }
Nous avons ici 3 fonctions que nous aimerions tester
- HandleLogin
- HandleRegister
- HandleGetUser
HandleGetUser
La fonction handleGetUser de ce gestionnaire récupère les détails de l'utilisateur en fonction d'un ID utilisateur fourni dans l'URL de la requête HTTP. Cela commence par extraire l'ID utilisateur des variables de chemin de requête à l'aide du routeur multiplexeur. Si l'ID utilisateur est manquant ou invalide (non entier), il répond par une erreur 400 Bad Request. Une fois validée, la fonction appelle la méthode GetUserByID sur le magasin de données pour récupérer les informations utilisateur. Si une erreur se produit lors de la récupération, elle renvoie une erreur interne du serveur 500. En cas de succès, il répond avec un statut 200 OK, envoyant les détails de l'utilisateur au format JSON dans le corps de la réponse.
Comme indiqué précédemment, pour tester les fonctions du gestionnaire, nous devons créer un routes_test.go. Voir le mien ci-dessous
test_project |__cmd |__api |__api.go |__main.go |__db |___seed.go |__internal |___db |___db.go |___services |___records |___routes_test.go |___routes.go |___store_test.go |___store.go |___user |___routes_test.go |___routes.go |___store_test.go |___store.go |__test_data |__docker-compose.yml |__Dockerfile |__Makefile
Notre fonction New Handler nécessite un magasin d'utilisateurs comme paramètre pour créer une structure de gestionnaire.
Puisque nous n'avons pas besoin d'un magasin réel, nous créons une structure fictive et créons des fonctions de récepteur qui se moquent de la fonction de la structure réelle. Nous faisons cela parce que nous traitons les tests de fonction du magasin séparément, nous n'avons donc pas besoin de tester cette partie du code dans les tests du gestionnaire.
La fonction de test TestGetUserHandler teste deux scénarios, le premier tente de récupérer un utilisateur sans fournir l'identifiant de l'utilisateur
// main.go package main import ( "log" "finance-crud-app/cmd/api" "finance-crud-app/internal/db" "github.com/gorilla/mux" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" ) type Server struct { db *sqlx.DB mux *mux.Router } func NewServer(db *sqlx.DB, mux *mux.Router) *Server { return &Server{ db: db, mux: mux, } } func main() { connStr := "postgres://postgres:Password123@localhost:5432/crud_db?sslmode=disable" dbconn, err := db.NewPGStorage(connStr) if err != nil { log.Fatal(err) } defer dbconn.Close() server := api.NewAPIServer(":8085", dbconn) if err := server.Run(); err != nil { log.Fatal(err) } }
Le test devrait réussir si la requête http répond avec un code d'état 400.
Le deuxième scénario de test concerne les cas où nous récupérons des informations utilisateur en utilisant l'URL correcte contenant un identifiant utilisateur valide. Dans ce cas de test, nous attendions une réponse avec un code d'état 200. Sinon, ce test aura échoué.
// api.go package api import ( "finance-crud-app/internal/services/records" "finance-crud-app/internal/services/user" "log" "net/http" "github.com/gorilla/mux" "github.com/jmoiron/sqlx" ) type APIServer struct { addr string db *sqlx.DB } func NewAPIServer(addr string, db *sqlx.DB) *APIServer { return &APIServer{ addr: addr, db: db, } } func (s *APIServer) Run() error { router := mux.NewRouter() subrouter := router.PathPrefix("/api/v1").Subrouter() userStore := user.NewStore(s.db) userHandler := user.NewHandler(userStore) userHandler.RegisterRoutes(subrouter) recordsStore := records.NewStore(s.db) recordsHandler := records.NewHandler(recordsStore, userStore) recordsHandler.RegisterRoutes(subrouter) log.Println("Listening on", s.addr) return http.ListenAndServe(s.addr, router) }
Conclusion
Nous avons réussi à implémenter des tests unitaires dans notre projet en créant des tests pour nos gestionnaires de routes. Nous avons vu comment utiliser des simulations pour tester uniquement une petite unité de code. Nous avons pu développer des tests d'intégration pour notre fonction qui interagissent avec la base de données Postgresql.
Si vous souhaitez avoir du temps avec le code du projet, clonez le dépôt depuis github ici
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Les caractéristiques principales de GO incluent la collection de déchets, la liaison statique et le support de concurrence. 1. Le modèle de concurrence du langage GO réalise une programmation concurrente efficace via le goroutine et le canal. 2. Les interfaces et les polymorphismes sont implémentés via des méthodes d'interface, de sorte que différents types peuvent être traités de manière unifiée. 3. L'utilisation de base démontre l'efficacité de la définition et de l'appel des fonctions. 4. Dans une utilisation avancée, les tranches offrent des fonctions puissantes de redimensionnement dynamique. 5. Des erreurs courantes telles que les conditions de course peuvent être détectées et résolues par l'imagerie. 6. Optimisation des performances Réutiliser les objets via Sync.Pool pour réduire la pression de collecte des ordures.

GO Language fonctionne bien dans la construction de systèmes efficaces et évolutifs. Ses avantages incluent: 1. Haute performance: compilé en code machine, vitesse de course rapide; 2. Programmation simultanée: simplifier le multitâche via les goroutines et les canaux; 3. Simplicité: syntaxe concise, réduction des coûts d'apprentissage et de maintenance; 4. Plate-forme multipliée: prend en charge la compilation multiplateforme, déploiement facile.

Confus quant au tri des résultats de la requête SQL. Dans le processus d'apprentissage de SQL, vous rencontrez souvent des problèmes déroutants. Récemment, l'auteur lit "Mick-SQL Basics" ...

La relation entre la convergence des piles technologiques et la sélection de la technologie dans le développement de logiciels, la sélection et la gestion des piles technologiques sont un problème très critique. Récemment, certains lecteurs ont proposé ...

Golang ...

Comment comparer et gérer trois structures en langue go. Dans la programmation GO, il est parfois nécessaire de comparer les différences entre deux structures et d'appliquer ces différences au ...

Comment afficher les packages installés à l'échelle mondiale dans Go? En train de se développer avec le langage Go, GO utilise souvent ...

Que dois-je faire si les étiquettes de structure personnalisées à Goland ne sont pas affichées? Lorsque vous utilisez Goland pour le développement du langage GO, de nombreux développeurs rencontreront des balises de structure personnalisées ...


Outils d'IA chauds

Undresser.AI Undress
Application basée sur l'IA pour créer des photos de nu réalistes

AI Clothes Remover
Outil d'IA en ligne pour supprimer les vêtements des photos.

Undress AI Tool
Images de déshabillage gratuites

Clothoff.io
Dissolvant de vêtements AI

AI Hentai Generator
Générez AI Hentai gratuitement.

Article chaud

Outils chauds

SublimeText3 version Mac
Logiciel d'édition de code au niveau de Dieu (SublimeText3)

SublimeText3 version chinoise
Version chinoise, très simple à utiliser

Envoyer Studio 13.0.1
Puissant environnement de développement intégré PHP

Bloc-notes++7.3.1
Éditeur de code facile à utiliser et gratuit

Adaptateur de serveur SAP NetWeaver pour Eclipse
Intégrez Eclipse au serveur d'applications SAP NetWeaver.