Retour au cours

backend / go

Tests : testing et table-driven tests

Leçon 171 exercice

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) de t.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).

FonctionComportementCas d'usage typique
t.ErrorfSignale l'échec, continue le testComparer un résultat attendu à un résultat obtenu
t.FatalfSignale l'échec, arrête le test immédiatementUne précondition invalide qui rendrait la suite absurde
t.RunCrée un sous-test nommé et indépendantTable-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.

go
// 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
}
go
// 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)
}
bash
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 HTML

Résumé

  • Un fichier _test.go, une fonction func TestXxx(t *testing.T).
  • Table-driven tests + t.Run = le pattern standard pour couvrir de nombreux cas lisiblement.
  • t.Errorf continue le test, t.Fatalf l'arrête immédiatement.
  • go test -race -cover ./... en CI est la base d'un pipeline Go fiable.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →