backend / go
Tests : testing et table-driven tests
Explication
Ce que vous allez apprendre
- Écrire un test unitaire avec le package standard
testing, sans dépendance externe - Distinguer
t.Errorf(continue le test) det.Fatalf(l'arrête immédiatement) - Écrire un table-driven test pour couvrir de nombreux cas de façon lisible
- Créer des sous-tests nommés avec
t.Run - Mesurer la couverture de code avec
go test -cover
Dans quel contexte ?
Un développeur modifie une fonction EstPremier pour corriger un bug sur le nombre 1, mais casse accidentellement le comportement pour le nombre 2 sans s'en rendre compte, car il n'a testé son changement qu'à la main dans un fmt.Println temporaire. Une suite de tests automatisés aurait immédiatement signalé cette régression avant même le commit — c'est exactement ce que cette leçon met en place.
D'abord, Go intègre son propre framework de test
Contrairement à beaucoup de langages qui nécessitent une bibliothèque tierce (JUnit, pytest, Jest...), Go inclut testing dans sa bibliothèque standard. Un fichier de test suit une convention stricte : il se termine par _test.go, et chaque fonction de test commence par Test, prend un unique paramètre *testing.T.
go test ./... découvre et exécute automatiquement tous ces fichiers dans tout le module, sans configuration supplémentaire à écrire.
Une fois un premier test écrit, une distinction importante s'impose
t.Errorf signale un échec mais laisse le test continuer à s'exécuter (utile pour voir tous les problèmes d'un coup). t.Fatalf, au contraire, arrête immédiatement le test en cours — indispensable après une erreur qui rendrait absurde la suite du test (par exemple, une erreur inattendue avant même de vérifier le résultat).
| Fonction | Comportement | Cas d'usage typique |
|---|---|---|
t.Errorf | Signale l'échec, continue le test | Comparer un résultat attendu à un résultat obtenu |
t.Fatalf | Signale l'échec, arrête le test immédiatement | Une précondition invalide qui rendrait la suite absurde |
t.Run | Crée un sous-test nommé et indépendant | Table-driven tests, cas isolés dans le rapport |
Prérequis
Il faut être à l'aise avec les structs anonymes et les slices (leçons précédentes) : le pattern table-driven repose sur un slice de structs représentant chaque cas de test.
Il reste un pattern à connaître absolument : le table-driven test
Plutôt que d'écrire une fonction de test séparée pour chaque cas (TestEstPremierZero, TestEstPremierDeux...), on définit un slice de structs représentant chaque cas (entrée, résultat attendu), puis on boucle dessus avec t.Run pour créer un sous-test indépendant par cas. C'est LE pattern de référence en Go pour tester de nombreuses variations d'une même fonction sans dupliquer de code.
Piège fréquent
Oublier de nommer chaque sous-test dans t.Run(c.nom, ...) avec un nom descriptif rend le rapport d'échec illisible : go test -v affichera juste des numéros au lieu d'une description du cas qui a échoué. Prends toujours le temps de nommer clairement chaque cas de test.
Enfin, comment savoir si les tests couvrent vraiment le code
Bonne pratique
Lance go test -cover ./... régulièrement, et génère un rapport HTML avec go test -coverprofile=c.out && go tool cover -html=c.out avant une revue de code importante. Une couverture élevée ne garantit pas l'absence de bugs, mais une couverture très basse signale à coup sûr des zones jamais vérifiées.
Maintenant que tu sais vérifier la correction de ton code, la prochaine leçon s'attaque à une question différente mais complémentaire : mesurer précisément sa performance grâce aux benchmarks.
Commandes & code
Tests : testing et table-driven tests
Go intègre son propre framework de test, sans dépendance externe requise.
// fichier: calculatrice.go
package calculatrice
import "errors"
func Diviser(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division par zero")
}
return a / b, nil
}
func EstPremier(n int) bool {
if n < 2 {
return false
}
for i := 2; i*i <= n; i++ {
if n%i == 0 {
return false
}
}
return true
}// fichier: calculatrice_test.go (convention: suffixe _test.go, meme package ou package_test)
package calculatrice
import "testing"
// Test simple : le nom DOIT commencer par Test, parametre *testing.T
func TestDiviser(t *testing.T) {
resultat, err := Diviser(10, 2)
if err != nil {
t.Fatalf("erreur inattendue: %v", err)
}
if resultat != 5 {
t.Errorf("attendu 5, obtenu %v", resultat)
}
}
func TestDiviserParZero(t *testing.T) {
_, err := Diviser(10, 0)
if err == nil {
t.Fatal("une erreur etait attendue pour une division par zero")
}
}
// Table-driven test : LE pattern idiomatique pour tester plusieurs cas
func TestEstPremier(t *testing.T) {
cas := []struct {
nom string
entree int
attendu bool
}{
{"nombre negatif", -5, false},
{"zero", 0, false},
{"un", 1, false},
{"deux, premier", 2, true},
{"quatre, non premier", 4, false},
{"dix-sept, premier", 17, true},
{"cent, non premier", 100, false},
}
for _, c := range cas {
// t.Run cree un sous-test nomme, visible individuellement dans le rapport
t.Run(c.nom, func(t *testing.T) {
resultat := EstPremier(c.entree)
if resultat != c.attendu {
t.Errorf("EstPremier(%d) = %v, attendu %v", c.entree, resultat, c.attendu)
}
})
}
}
// Setup/teardown avec TestMain (execute une seule fois pour tout le fichier)
func TestMain(m *testing.M) {
// setup avant tous les tests
code := m.Run()
// teardown apres tous les tests
os.Exit(code)
}go test ./... # lancer tous les tests
go test -v ./... # mode verbeux, affiche chaque sous-test
go test -run TestEstPremier # filtrer par nom
go test -cover ./... # pourcentage de couverture
go test -coverprofile=c.out && go tool cover -html=c.out # rapport HTMLRésumé
- Un fichier
_test.go, une fonctionfunc TestXxx(t *testing.T). - Table-driven tests +
t.Run= le pattern standard pour couvrir de nombreux cas lisiblement. t.Errorfcontinue le test,t.Fatalfl'arrête immédiatement.go test -race -cover ./...en CI est la base d'un pipeline Go fiable.
Exercices pratiques
Mission : une correction pour le nombre 1 qui casse silencieusement le nombre 2
Objectif : Écrire un table-driven test qui aurait détecté une régression manuelle, puis raisonner sur les limites d'une couverture de test apparemment complète.
Contexte
Un développeur corrige EstPremier pour que EstPremier(1) retourne bien false (c'était true par erreur avant). Il vérifie son changement uniquement avec un fmt.Println(EstPremier(1)) temporaire dans main, puis commit. Deux jours plus tard, un autre développeur découvre en production que EstPremier(2) retourne maintenant false au lieu de true : le premier correctif avait introduit une régression silencieuse sur ce cas précis.