Test unitaires en Go : Structure et bonnes pratiques
Le test en Go des bases aux modèles avancés
Le package de test intégré à Go fournit un cadre puissant et minimaliste pour écrire des tests unitaires sans dépendances externes. Voici les fondamentaux des tests, la structure du projet et les modèles avancés pour construire des applications Go fiables.

Pourquoi les tests sont importants en Go
La philosophie de Go met l’accent sur la simplicité et la fiabilité. La bibliothèque standard inclut le package testing, rendant les tests unitaires un citoyen de première classe dans l’écosystème Go. Le code Go bien testé améliore la maintenabilité, détecte les bugs tôt et fournit une documentation via des exemples. Si vous êtes nouveau en Go, consultez notre Feuille de triche Go pour un résumé rapide des fondamentaux du langage.
Les avantages principaux des tests en Go :
- Support intégré : Aucun framework externe requis
- Exécution rapide : Exécution par défaut en parallèle
- Syntaxe simple : Peu de code de mise en place
- Outils riches : Rapports de couverture, benchmarks et profilage
- Ami des pipelines CI/CD : Intégration facile avec les pipelines automatisés
Structure du projet pour les tests Go
Les tests Go vivent à côté de votre code de production avec une convention de nommage claire :
myproject/
├── go.mod
├── main.go
├── calculator.go
├── calculator_test.go
├── utils/
│ ├── helper.go
│ └── helper_test.go
└── models/
├── user.go
└── user_test.go
Conventions clés :
- Les fichiers de test se terminent par
_test.go - Les tests sont dans le même package que le code (ou utilisent le suffixe
_testpour les tests boîte noire) - Chaque fichier source peut avoir un fichier de test correspondant
Approches de test par package
Test boîte blanche (même package) :
package calculator
import "testing"
// Peut accéder aux fonctions et variables non exportées
Test boîte noire (package externe) :
package calculator_test
import (
"testing"
"myproject/calculator"
)
// Peut accéder uniquement aux fonctions exportées (recommandé pour les API publiques)
Structure de base des tests
Chaque fonction de test suit ce modèle :
package calculator
import "testing"
// La fonction de test doit commencer par "Test"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
Méthodes de testing.T :
t.Error()/t.Errorf(): Marque le test comme échoué mais continuet.Fatal()/t.Fatalf(): Marque le test comme échoué et s’arrête immédiatementt.Log()/t.Logf(): Journalisation (affichée uniquement avec le drapeau-v)t.Skip()/t.Skipf(): Ignorer le testt.Parallel(): Exécuter le test en parallèle avec d’autres tests parallèles
Tests basés sur des tableaux : La manière Go
Les tests basés sur des tableaux sont l’approche idiomatique en Go pour tester plusieurs scénarios. Avec les générics Go, vous pouvez également créer des helpers de test type-sûrs qui fonctionnent avec différents types de données :
func TestCalculate(t *testing.T) {
tests := []struct {
name string
a, b int
op string
expected int
wantErr bool
}{
{"addition", 2, 3, "+", 5, false},
{"subtraction", 5, 3, "-", 2, false},
{"multiplication", 4, 3, "*", 12, false},
{"division", 10, 2, "/", 5, false},
{"division par zéro", 10, 0, "/", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Calculate(tt.a, tt.b, tt.op)
if (err != nil) != tt.wantErr {
t.Errorf("Calculate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if result != tt.expected {
t.Errorf("Calculate(%d, %d, %q) = %d; want %d",
tt.a, tt.b, tt.op, result, tt.expected)
}
})
}
}
Avantages :
- Une seule fonction de test pour plusieurs scénarios
- Facile à ajouter de nouveaux cas de test
- Documentation claire du comportement attendu
- Meilleure organisation et maintenabilité des tests
Exécution des tests
Commandes de base
# Exécuter les tests dans le répertoire actuel
go test
# Exécuter les tests avec sortie détaillée
go test -v
# Exécuter les tests dans tous les sous-répertoires
go test ./...
# Exécuter un test spécifique
go test -run TestAdd
# Exécuter des tests correspondant à un motif
go test -run TestCalculate/addition
# Exécuter les tests en parallèle (par défaut GOMAXPROCS)
go test -parallel 4
# Exécuter les tests avec un délai
go test -timeout 30s
Couverture des tests
# Exécuter les tests avec couverture
go test -cover
# Générer le profil de couverture
go test -coverprofile=coverage.out
# Afficher la couverture dans le navigateur
go tool cover -html=coverage.out
# Afficher la couverture par fonction
go tool cover -func=coverage.out
# Définir le mode de couverture (set, count, atomic)
go test -covermode=count -coverprofile=coverage.out
Drapeaux utiles
-short: Exécuter les tests marqués avecif testing.Short()-race: Activer le détecteur de course (détecte les problèmes d’accès concurrent)-cpu: Spécifier les valeurs de GOMAXPROCS-count n: Exécuter chaque test n fois-failfast: Arrêter au premier échec de test
Helpers de test et configuration/Nettoyage
Fonctions helpers
Marquer les fonctions helpers avec t.Helper() pour améliorer le rapport d’erreur :
func assertEqual(t *testing.T, got, want int) {
t.Helper() // Cette ligne est rapportée comme l'appelant
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestMath(t *testing.T) {
result := Add(2, 3)
assertEqual(t, result, 5) // La ligne d'erreur pointe ici
}
Configuration et nettoyage
func TestMain(m *testing.M) {
// Code de configuration
setup()
// Exécuter les tests
code := m.Run()
// Code de nettoyage
teardown()
os.Exit(code)
}
Fixtures de test
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("setup test case")
return func(t *testing.T) {
t.Log("teardown test case")
}
}
func TestSomething(t *testing.T) {
teardown := setupTestCase(t)
defer teardown(t)
// Code de test ici
}
Mocking et injection de dépendances
Mocking basé sur les interfaces
Lorsque vous testez du code qui interagit avec des bases de données, l’utilisation d’interfaces facilite la création d’implémentations de mock. Si vous travaillez avec PostgreSQL en Go, consultez notre comparaison des ORMs Go pour choisir la bonne bibliothèque de base de données avec une bonne testabilité.
// Code de production
type Database interface {
GetUser(id int) (*User, error)
}
type UserService struct {
db Database
}
func (s *UserService) GetUserName(id int) (string, error) {
user, err := s.db.GetUser(id)
if err != nil {
return "", err
}
return user.Name, nil
}
// Code de test
type MockDatabase struct {
users map[int]*User
}
func (m *MockDatabase) GetUser(id int) (*User, error) {
if user, ok := m.users[id]; ok {
return user, nil
}
return nil, errors.New("user not found")
}
func TestGetUserName(t *testing.T) {
mockDB := &MockDatabase{
users: map[int]*User{
1: {ID: 1, Name: "Alice"},
},
}
service := &UserService{db: mockDB}
name, err := service.GetUserName(1)
if err != nil {
t.Fatalf("erreur inattendue : %v", err)
}
if name != "Alice" {
t.Errorf("got %s, want Alice", name)
}
}
Bibliothèques de test populaires
Testify
La bibliothèque de test la plus populaire en Go pour les affirmations et les mocks :
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestWithTestify(t *testing.T) {
result := Add(2, 3)
assert.Equal(t, 5, result, "ils devraient être égaux")
assert.NotNil(t, result)
}
// Exemple de mock
type MockDB struct {
mock.Mock
}
func (m *MockDB) GetUser(id int) (*User, error) {
args := m.Called(id)
return args.Get(0).(*User), args.Error(1)
}
Autres outils
- gomock : Framework de mocking de Google avec génération de code
- httptest : Bibliothèque standard pour tester les gestionnaires HTTP
- testcontainers-go : Tests d’intégration avec des conteneurs Docker
- ginkgo/gomega : Framework de test en style BDD
Lorsque vous testez des intégrations avec des services externes comme des modèles d’IA, vous devrez créer des mocks ou des stubs pour ces dépendances. Par exemple, si vous utilisez Ollama en Go, envisagez de créer des wrappers d’interface pour rendre votre code plus testable.
Tests de benchmark
Go inclut un support intégré pour les benchmarks :
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// Exécuter les benchmarks
// go test -bench=. -benchmem
La sortie affiche les itérations par seconde et les allocations de mémoire.
Bonnes pratiques
- Écrivez des tests basés sur des tableaux : Utilisez le modèle de slice de structs pour plusieurs cas de test
- Utilisez
t.Runpour les sous-tests : Meilleure organisation et possibilité d’exécuter sélectivement les sous-tests - Testez d’abord les fonctions exportées : Concentrez-vous sur le comportement de l’API publique
- Gardez les tests simples : Chaque test doit vérifier une seule chose
- Utilisez des noms de tests significatifs : Décrivez ce qui est testé et le résultat attendu
- Ne testez pas les détails d’implémentation : Testez le comportement, pas les internes
- Utilisez des interfaces pour les dépendances : Facilite le mocking
- Visez une forte couverture, mais qualité avant quantité : Une couverture de 100 % ne signifie pas l’absence de bugs
- Exécutez les tests avec le drapeau -race : Détectez les problèmes de concurrence tôt
- Utilisez
TestMainpour les configurations coûteuses : Évitez de répéter la configuration dans chaque test
Exemple : Suite de tests complète
package user
import (
"errors"
"testing"
)
type User struct {
ID int
Name string
Email string
}
func ValidateUser(u *User) error {
if u.Name == "" {
return errors.New("le nom ne peut pas être vide")
}
if u.Email == "" {
return errors.New("l'adresse e-mail ne peut pas être vide")
}
return nil
}
// Fichier de test : user_test.go
func TestValidateUser(t *testing.T) {
tests := []struct {
name string
user *User
wantErr bool
errMsg string
}{
{
name: "utilisateur valide",
user: &User{ID: 1, Name: "Alice", Email: "alice@example.com"},
wantErr: false,
},
{
name: "nom vide",
user: &User{ID: 1, Name: "", Email: "alice@example.com"},
wantErr: true,
errMsg: "le nom ne peut pas être vide",
},
{
name: "e-mail vide",
user: &User{ID: 1, Name: "Alice", Email: ""},
wantErr: true,
errMsg: "l'adresse e-mail ne peut pas être vide",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateUser(tt.user)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && err.Error() != tt.errMsg {
t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
}
})
}
}
Liens utiles
- Documentation officielle du package de test Go
- Blog Go : Tests basés sur des tableaux
- Référentiel GitHub Testify
- Documentation GoMock
- Apprendre Go avec des tests
- Outil de couverture de code Go
- Feuille de triche Go
- Comparaison des ORMs Go pour PostgreSQL : GORM vs Ent vs Bun vs sqlc
- SDKs Go pour Ollama - comparaison avec des exemples
- Applications CLI en Go avec Cobra & Viper
- Générics Go : cas d’utilisation et modèles
Conclusion
Le framework de test de Go fournit tout ce dont on a besoin pour des tests unitaires complets avec un minimum de configuration. En suivant les idiomes Go comme les tests basés sur des tableaux, en utilisant des interfaces pour le mocking et en exploitant les outils intégrés, vous pouvez créer des ensembles de tests maintenables et fiables qui évoluent avec votre codebase.
Ces pratiques de test s’appliquent à tous les types d’applications Go, des services web aux applications CLI construites avec Cobra & Viper. Le test des outils de ligne de commande nécessite des schémas similaires avec une attention particulière au test d’entrée/sortie et à l’analyse des drapeaux.
Commencez par des tests simples, ajoutez progressivement la couverture et souvenez-vous que le test est un investissement dans la qualité du code et la confiance des développeurs. L’accent mis par la communauté Go sur les tests rend plus facile la maintenance des projets à long terme et la collaboration efficace avec les membres de l’équipe.