Le code testé
Les outils de test de Go font partie de la chaîne d'outils standard : le package testing et la commande go test. Aucun framework à installer, aucune bibliothèque d'assertions nécessaire.
Les exemples de cette page testent une petite fonction, Slugify, qui transforme un titre en slug d'URL. La voici, exécutable, avec une vérification faite à la main dans main :
L'éditeur du navigateur exécute main ; il ne peut pas lancer go test. Tout ce qui suit est écrit tel que cela vit dans un vrai projet, avec la sortie du terminal affichée après chaque commande. Tout a été exécuté avec Go 1.24.
Le premier test
Dans un projet, la fonction vit dans un package, et ses tests vivent à côté, dans un fichier dont le nom se termine par _test.go :
textutil/
├── go.mod module example.com/textutil
├── slug.go package textutil, func Slugify
└── slug_test.go package textutil, the tests
// slug_test.go
package textutil
import "testing"
func TestSlugify(t *testing.T) {
got := Slugify("Hello, World!")
want := "hello-world"
if got != want {
t.Errorf("Slugify(%q) = %q, want %q", "Hello, World!", got, want)
}
}
Les règles qu'utilise go test :
- Seuls les fichiers qui se terminent par
_test.gosont des fichiers de test.go buildles ignore, donc le code de test ne se retrouve jamais dans votre binaire. - Un test est une fonction nommée
TestXxx(la partie aprèsTestne doit pas commencer par une minuscule) qui prend un seul*testing.T. - Un test réussit sauf s'il appelle l'une des méthodes d'échec ou provoque un panic.
go test # the package in the current directory
go test ./... # every package in the module
$ go test
PASS
ok example.com/textutil 0.318s
Le format de message d'échec Func(input) = got, want expected est la convention Go. Mettez l'entrée dans le message : un échec qui dit seulement got "a", want "b" vous renvoie au code pour savoir de quelle entrée il s'agissait.
t.Errorf ou t.Fatalf
| Méthode | Marque l'échec | Arrête le test |
|---|---|---|
t.Error, t.Errorf | oui | non, continue |
t.Fatal, t.Fatalf | oui | oui, immédiatement |
t.Log, t.Logf | non | non, n'affiche qu'avec -v ou en cas d'échec |
t.Skip, t.Skipf | non | oui, signalé comme ignoré |
Utilisez Errorf par défaut, pour qu'une exécution signale chaque champ faux. Utilisez Fatalf quand le reste du test ne peut pas fonctionner, typiquement après une erreur inattendue :
func TestCountWords(t *testing.T) {
path := writeFile(t, "doc.txt", "the quick brown\nfox jumps\n")
got, err := CountWords(path)
if err != nil {
t.Fatalf("CountWords: unexpected error: %v", err) // stop: got is meaningless
}
if got != 5 {
t.Errorf("CountWords = %d, want 5", got)
}
}
Fatal arrête le test en appelant runtime.Goexit, donc il doit être appelé depuis la goroutine du test elle-même. Depuis une goroutine que vous avez lancée, signalez avec t.Error et retournez.
Tests pilotés par tableau avec t.Run
La plupart des tests Go sont des tableaux : une slice de cas, une boucle, et t.Run pour donner à chaque cas son propre nom.
func TestSlugifyTable(t *testing.T) {
tests := []struct {
name, in, want string
}{
{"simple", "Go Testing", "go-testing"},
{"punctuation", "What's new in Go 1.24?", "what-s-new-in-go-1-24"},
{"extra spaces", " lots of space ", "lots-of-space"},
{"non-ascii dropped", "Café au lait", "caf-au-lait"},
{"empty", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Slugify(tt.in); got != tt.want {
t.Errorf("Slugify(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
Ajouter un cas tient en une ligne. Chaque sous-test est signalé séparément, un Fatal dans l'un d'eux ne termine que ce sous-test, et vous pouvez en lancer un seul par son nom. Lancez tout avec -v :
$ go test -v
=== RUN TestSlugify
--- PASS: TestSlugify (0.00s)
=== RUN TestSlugifyTable
=== RUN TestSlugifyTable/simple
=== RUN TestSlugifyTable/punctuation
=== RUN TestSlugifyTable/extra_spaces
=== RUN TestSlugifyTable/non-ascii_dropped
=== RUN TestSlugifyTable/empty
--- PASS: TestSlugifyTable (0.00s)
--- PASS: TestSlugifyTable/simple (0.00s)
--- PASS: TestSlugifyTable/punctuation (0.00s)
--- PASS: TestSlugifyTable/extra_spaces (0.00s)
--- PASS: TestSlugifyTable/non-ascii_dropped (0.00s)
--- PASS: TestSlugifyTable/empty (0.00s)
=== RUN TestCountWords
--- PASS: TestCountWords (0.00s)
=== RUN TestCountWordsMissingFile
--- PASS: TestCountWordsMissingFile (0.00s)
=== RUN ExampleSlugify
--- PASS: ExampleSlugify (0.00s)
PASS
ok example.com/textutil 0.182s
Les espaces dans les noms de sous-tests deviennent des underscores. Quand un cas échoue, la sortie nomme le sous-test et la ligne :
--- FAIL: TestSlugifyTable (0.00s)
--- FAIL: TestSlugifyTable/underscore (0.00s)
slug_test.go:27: Slugify("snake_case_name") = "snake-case-name", want "snake_case_name"
FAIL
FAIL example.com/textutil 0.289s
Depuis Go 1.22, chaque itération de boucle a son propre tt, donc les closures dans t.Run voient le bon cas même quand les sous-tests s'exécutent en parallèle. Le code plus ancien contient souvent tt := tt en haut du corps de la boucle pour cette raison ; ce n'est plus nécessaire.
Pour exécuter des sous-tests en parallèle, appelez t.Parallel() au début de la fonction du sous-test. Ne le faites que lorsque les cas sont indépendants et assez lents pour que cela compte.
Les flags de go test que vous utiliserez
| Commande | Effet |
|---|---|
go test -v | affiche le nom et le résultat de chaque test ainsi que la sortie de t.Log |
go test -run TestSlugify | lance les tests dont le nom correspond à l'expression régulière |
go test -run 'TestSlugifyTable/empty' | lance un seul sous-test |
go test -count=1 | ignore les résultats en cache et relance |
go test -race | lance avec le détecteur de data races |
go test -short | demande aux tests longs de s'ignorer eux-mêmes (if testing.Short() { t.Skip() }) |
go test -failfast | s'arrête après le premier test en échec |
go test -timeout 30s | échoue si l'exécution dure plus longtemps (10 minutes par défaut) |
go test -cover | affiche la couverture des instructions |
go test -bench=. | lance aussi les benchmarks |
Quand vous nommez des packages (go test ., go test ./...), go test met en cache les résultats des packages dont le code et les entrées n'ont pas changé et affiche (cached) à côté. Un simple go test sans argument ne met jamais en cache. Le cache suit les fichiers et les variables d'environnement qu'un test lit via le package os, mais pas un état extérieur comme une base de données ou un service réseau, donc un test qui en dépend peut réussir depuis le cache alors qu'il échouerait maintenant ; -count=1 force une vraie exécution.
Helpers, répertoires temporaires et nettoyage
L'appel à writeFile dans TestCountWords ci-dessus est un helper de test :
// writeFile is a test helper: t.Helper makes failures point at the caller.
func writeFile(t *testing.T, name, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), name) // removed automatically after the test
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("writing %s: %v", name, err)
}
return path
}
t.Helper()marque la fonction comme helper, pour qu'un échec soit signalé à la ligne du test qui l'a appelée, pas dans le helper.t.TempDir()crée un répertoire neuf et le supprime à la fin du test. Les tests n'ont jamais besoin de nettoyer eux-mêmes leurs fichiers.t.Cleanup(func() { ... })enregistre tout autre démontage (fermer un serveur, supprimer une table). Les nettoyages s'exécutent après le test et ses sous-tests, le dernier enregistré en premier.t.Setenv("KEY", "value")définit une variable d'environnement pour ce test uniquement et la restaure ensuite.t.Context()(Go 1.24) renvoie un context annulé juste avant l'exécution des nettoyages.
Les fichiers de fixtures vont dans un répertoire nommé testdata à côté des tests. L'outil go l'ignore en tant que package, et les tests s'exécutent avec le répertoire du package comme répertoire de travail, donc os.ReadFile("testdata/input.json") fonctionne.
Tester les erreurs et les handlers HTTP
Vérifiez les erreurs avec errors.Is ou errors.As, comme le ferait le code appelant :
func TestCountWordsMissingFile(t *testing.T) {
_, err := CountWords(filepath.Join(t.TempDir(), "nope.txt"))
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("err = %v, want fs.ErrNotExist", err)
}
}
Comparer des chaînes d'erreur casse dès que quelqu'un reformule un message.
Pour les handlers HTTP, net/http/httptest vous donne un faux ResponseWriter pour appeler le handler directement, sans réseau :
func TestHealth(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
healthHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
if body := rec.Body.String(); body != "ok\n" {
t.Errorf("body = %q, want %q", body, "ok\n")
}
}
httptest.NewServer démarre un vrai serveur sur localhost pour tester du code client. La page sur le client HTTP l'utilise pour chaque exemple.
Couverture
$ go test -cover
PASS
coverage: 100.0% of statements
ok example.com/textutil 0.578s
$ go test -coverprofile=cover.out
$ go tool cover -func=cover.out
example.com/textutil/slug.go:11: Slugify 100.0%
example.com/textutil/words.go:9: CountWords 100.0%
total: (statements) 100.0%
$ go tool cover -html=cover.out # opens a browser with covered lines in green
La couverture vous dit quelles lignes ne se sont jamais exécutées, ce qui est utile pour trouver des branches non testées. Un chiffre élevé ne dit pas si les assertions valent quelque chose : un test qui appelle chaque fonction et ne vérifie rien atteint 100 %.
Benchmarks
Un benchmark est une fonction func BenchmarkXxx(b *testing.B). Go 1.24 a ajouté b.Loop, qui est désormais la forme recommandée :
func BenchmarkSlugify(b *testing.B) {
for b.Loop() {
Slugify("The Go Programming Language, 2nd Edition")
}
}
b.Loop exécute le corps autant de fois que nécessaire pour une mesure stable, exclut du chronométrage le code de préparation situé avant la boucle, et empêche le compilateur d'éliminer l'appel par optimisation. Le code écrit avant Go 1.24 utilise for i := 0; i < b.N; i++, qui fonctionne toujours mais demande b.ResetTimer() après une préparation coûteuse et peut être trompé par l'élimination de code mort.
Les benchmarks ne s'exécutent pas avec un simple go test. Demandez-les, et sautez les tests ordinaires avec -run='^$' :
$ go test -bench=. -benchmem -run='^$'
goos: darwin
goarch: arm64
pkg: example.com/textutil
cpu: Apple M4
BenchmarkSlugify-10 5061945 238.1 ns/op 168 B/op 5 allocs/op
PASS
ok example.com/textutil 1.410s
Les colonnes sont : le nom suivi de GOMAXPROCS, le nombre d'itérations exécutées, le temps par appel, les octets alloués par appel, les allocations par appel. Les chiffres dépendent de la machine ; comparez des exécutions sur la même. Pour comparer de façon fiable avant et après une modification, lancez chaque côté plusieurs fois avec -count=10 et donnez les deux sorties à benchstat (golang.org/x/perf/cmd/benchstat).
Les exemples sont aussi des tests
Une fonction d'exemple affiche quelque chose et déclare la sortie attendue dans un commentaire. go test l'exécute et échoue si la sortie diffère, et go doc et pkg.go.dev l'affichent comme documentation :
// example_test.go
package textutil_test
import (
"fmt"
"example.com/textutil"
)
func ExampleSlugify() {
fmt.Println(textutil.Slugify("Hello, World!"))
// Output: hello-world
}
Le nom de package textutil_test en fait un test externe : il ne peut utiliser que l'API exportée, comme un vrai appelant. De tels fichiers peuvent se trouver dans le même répertoire que le package. Sans commentaire // Output:, l'exemple est compilé mais pas exécuté. Utilisez // Unordered output: quand les lignes peuvent apparaître dans n'importe quel ordre.
Le fuzzing, en bref
Go 1.18 a ajouté les tests de fuzzing, qui génèrent des entrées pour trouver des plantages et des invariants cassés :
func FuzzSlugify(f *testing.F) {
f.Add("Hello, World!") // seed input
f.Fuzz(func(t *testing.T, s string) {
slug := Slugify(s)
if strings.Contains(slug, "--") || strings.HasPrefix(slug, "-") {
t.Errorf("Slugify(%q) = %q: bad hyphens", s, slug)
}
})
}
Un simple go test n'exécute que les entrées de départ (les valeurs de f.Add et les fichiers enregistrés sous testdata/fuzz). go test -fuzz=FuzzSlugify continue d'en générer de nouvelles jusqu'à trouver un échec ou jusqu'à ce que vous l'arrêtiez, et enregistre les entrées en échec sous testdata/fuzz pour qu'elles deviennent des cas de test ordinaires.
Erreurs courantes
- Un fichier de test mal placé ou mal nommé. Il doit se terminer par
_test.goet se trouver dans le répertoire du package.slug_tests.goest compilé dans le package, pas exécuté comme test. - Une minuscule après
Test.Testslugifyn'est pas un test.TestSlugifyetTest_slugifyen sont. t.Fataldepuis une autre goroutine. Signalez avect.Errorà cet endroit, et attendez la goroutine avant que le test ne retourne.- Des messages sans l'entrée. Indiquez ce qui a été passé, ce qui est sorti, et ce qui était attendu.
- Se fier à une réussite en cache. Utilisez
-count=1quand un test dépend de quoi que ce soit en dehors du package. - Des tests qui dépendent de l'ordre des autres ou de variables globales partagées. Chaque test doit préparer son propre état, c'est à cela que servent
t.TempDir,t.Setenvett.Cleanup.
Questions fréquentes
Comment écrire un test unitaire en Go ?
Créez un fichier qui se termine par _test.go dans le même répertoire que le code, importez testing, et écrivez une fonction func TestName(t *testing.T) qui appelle votre code et signale les écarts avec t.Errorf. Lancez go test dans ce répertoire, ou go test ./... pour tout le module. Aucun framework ni bibliothèque d'assertions n'est nécessaire.
Quelle est la différence entre t.Error et t.Fatal en Go ?
t.Error et t.Errorf marquent le test comme échoué et continuent son exécution, donc une exécution peut signaler plusieurs problèmes. t.Fatal et t.Fatalf le marquent comme échoué et arrêtent le test immédiatement. Utilisez Fatal quand continuer n'a pas de sens, par exemple après une erreur inattendue qui rend le résultat inutilisable.
Comment lancer un seul test en Go ?
Passez une expression régulière à -run : go test -run TestSlugify lance tous les tests dont le nom correspond, et go test -run 'TestSlugifyTable/punctuation' lance un seul sous-test (les espaces dans les noms de sous-tests deviennent des underscores). Ajoutez -v pour voir le nom et le résultat de chaque test, et -count=1 pour contourner le cache des tests.
Comment écrire un benchmark en Go ?
Écrivez func BenchmarkName(b *testing.B) dans un fichier _test.go et placez le code à mesurer dans une boucle for b.Loop() { ... } (Go 1.24 ; le code plus ancien utilise for i := 0; i < b.N; i++). Lancez-le avec go test -bench=. -benchmem, qui indique les nanosecondes, les octets et les allocations par opération.
Comment voir la couverture des tests en Go ?
go test -cover affiche le pourcentage d'instructions exécutées par les tests. Pour le détail, écrivez un profil avec go test -coverprofile=cover.out, puis lancez go tool cover -func=cover.out pour les chiffres par fonction ou go tool cover -html=cover.out pour voir les lignes couvertes et non couvertes dans un navigateur.