Leçons du module (2/5)
Tests pilotés par les données
Les tests basés sur des tables sont le modèle idiomatique de Go : au lieu d'écrire N fonctions TestQuestoCasoSpecifico, vous déclarez une tranche de cas et d'itérations. Le code d'assertion n'est qu'UN, les données de test sont distinctes de la logique.
Le schéma de base
func TestAbs(t *testing.T) {
cases := []struct {
name string
in int
want int
}{
{"pos", 3, 3},
{"neg", -3, 3},
{"zero", 0, 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Abs(tc.in); got != tc.want {
t.Errorf("Abs(%d) = %d; voglio %d", tc.in, got, tc.want)
}
})
}
}Trois ingrédients :
- Tranche de structure anonyme avec des champs typiques :
name, les entrées, la sortie attendue (et peut-être une erreur attendue). for _, tc := range cases— la boucle d'exécution.t.Run(tc.name, func(t *testing.T) { ... })— chaque cas devient un sous-test avec son propre nom.
Pourquoi t.Run ?
t.Run crée un sous-test qui a :
- Nom individuel affiché dans le résultat :
TestAbs/pos,TestAbs/neg, ... t *testing.Tproprement dit : une panne dans un cas ne bloque pas les autres (sauf sit.Fatal).- Filtre de ligne de commande :
go test -run TestAbs/negpour exécuter ce cas uniquement. - Parallélisme facultatif : appelez
t.Parallel()à l'intérieur de la fonction pour paralléliser.
go test -v -run TestAbs/negCas avec erreur attendue
cases := []struct {
name string
in string
want int
wantErr bool
}{
{"ok", "42", 42, false},
{"bad", "x", 0, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := Atoi(tc.in)
if (err != nil) != tc.wantErr {
t.Fatalf("err = %v, wantErr = %v", err, tc.wantErr)
}
if !tc.wantErr && got != tc.want {
t.Errorf("got %d, want %d", got, tc.want)
}
})
}Piège de la fermeture de boucle (avant Go 1.22)
Dans les versions antérieures à Go 1.22, la variable tc était partagée entre les itérations : l'appel de t.Parallel() à l'intérieur de t.Run entraînait l'exécution de tous les sous-tests sur le même dernier cas. La solution était :
for _, tc := range cases {
tc := tc // rebind locale (pre-1.22)
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// ...
})
}Dénomination des cas
Bonnes pratiques :
- Nom court mais évocateur :
"empty input","too large","negative". - Pas d'espace si vous souhaitez filtrer facilement avec
-run(ou utiliser des traits de soulignement). - Incluez toujours un cas "happy path", un cas "edge" (zéro, vide) et au moins un cas d'erreur.
Exercices
Définissez une structure anonyme de tranches de cas avec les champs in et Want, remplie de 3 cas pour tester Abs (positif, négatif, zéro).
Solution disponible après 3 tentatives
Ajoutez t.Run avec tc.name pour transformer chaque cas en un sous-test nommé.
Solution disponible après 3 tentatives
Quel est le principal avantage du modèle piloté par table par rapport à N fonctions de test distinctes ?
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { ... })
}