Introduction : Pourquoi les tests en Go sont-ils importants ?
Go (Golang) est un langage qui, dès sa conception, a été orienté vers la simplicité, la performance et la fiabilité. Le package intégré testing et des outils puissants comme go test font de l'écriture de tests une partie intégrante du développement. Sans tests de qualité, même le code le plus élégant se transforme avec le temps en un système « fragile », où une seule modification casse tout.
Dans cet article, nous allons détailler trois niveaux clés de test : unit (tests unitaires), integration (tests d'intégration) et e2e (end-to-end). Vous apprendrez en quoi ils diffèrent, quand les appliquer et comment écrire des tests efficaces en Go. Ce contenu sera utile aussi bien aux débutants qu'aux développeurs expérimentés souhaitant systématiser leurs connaissances.
1. Tests unitaires
Les tests unitaires sont le fondement de la pyramide des tests. Ils vérifient le fonctionnement de fonctions, méthodes ou modules individuels, isolés des dépendances externes (base de données, réseau, système de fichiers).
Principes de base des tests unitaires en Go
- Isolation : Le code testé ne doit pas dépendre de services externes.
- Rapidité : Les tests unitaires s'exécutent en quelques millisecondes.
- Déterminisme : Le résultat du test ne doit pas changer d'une exécution à l'autre.
Exemple d'un test unitaire simple
Supposons que nous ayons une fonction pour additionner deux nombres :
// math.gopackage math
func Add(a, b int) int { return a + b}Le test pour cette fonction ressemblera à ceci :
// math_test.gopackage math
import "testing"
func TestAdd(t *testing.T) { result := Add(2, 3) expected := 5
if result != expected { t.Errorf("Add(2, 3) = %d; want %d", result, expected) }}Le test se lance avec la commande : go test ./...
Tests tabulaires (Table-driven tests)
La communauté Go préfère les tests tabulaires pour vérifier de nombreux cas :
func TestAddTable(t *testing.T) { tests := []struct { a, b, expected int }{ {1, 1, 2}, {0, 0, 0}, {-1, 1, 0}, {100, 200, 300}, }
for _, tt := range tests { result := Add(tt.a, tt.b) if result != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) } }}Conseil : Utilisez t.Run() pour créer des tests imbriqués avec des noms explicites — cela améliore la lisibilité des rapports.
2. Tests d'intégration
Les tests d'intégration vérifient l'interaction entre les composants du système : base de données, API, services externes. Contrairement aux tests unitaires, on utilise ici des dépendances réelles (ou presque réelles).
Spécificités des tests d'intégration en Go
- Nécessitent souvent la configuration d'un environnement (par exemple, une base de données de test).
- Peuvent utiliser des build tags pour séparer les tests unitaires et d'intégration.
- S'exécutent plus lentement, donc on les lance séparément.
Exemple : Test d'interaction avec une base de données (PostgreSQL)
Créons un fichier avec le build tag integration :
// db_test.go//go:build integration
package db
import ( "database/sql" "testing")
func TestSaveUser(t *testing.T) { // Connexion à la base de données de test dsn := "host=localhost user=test password=test dbname=testdb sslmode=disable" db, err := sql.Open("postgres", dsn) if err != nil { t.Fatalf("failed to connect to db: %v", err) } defer db.Close()
// Nettoyage des données avant le test _, _ = db.Exec("TRUNCATE TABLE users")
// Exécution de l'opération testée user := User{Name: "Alice", Email: "alice@example.com"}