Le package testing
Go a le testing intégré dans le langage. Pas besoin d'installer Jest, pytest ou PHPUnit. Tout est déjà là.
Règles simples :
- Le fichier de test s'appelle
*_test.go(ex :math_test.go) - Les fonctions de test commencent par
Test(majuscule) - Elles prennent un paramètre
*testing.T
En Go, comment l'outil go test sait-il qu'une fonction est un test ? Faut-il une annotation ou un décorateur comme dans d'autres langages ? Avant de dérouler : quelle différence entre t.Error et t.Fatal dans un test ?
Voir la réponse
Il n'y a aucune annotation ni décorateur en Go. go test repère les tests par convention : une fonction de test doit s'appeler TestXxx (le mot Test suivi d'un nom commençant par une majuscule) et prendre un unique paramètre t *testing.T, dans un fichier dont le nom se termine par _test.go. Le nom et la signature suffisent, l'outil les découvre automatiquement. Pour signaler un échec : t.Error (ou t.Errorf) marque le test comme échoué mais continue l'exécution du reste de la fonction ; t.Fatal (ou t.Fatalf) marque l'échec et stoppe immédiatement la fonction de test, utile quand la suite n'a plus de sens (par exemple après une erreur d'initialisation).
// math.go
package math
func Add(a, b int) int {
return a + b
}
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d, attendu 5", result)
}
}
Lancez les tests avec go test :
$ go test
PASS
ok myproject/math 0.002s
0.002 secondes. Pas de setup, pas de configuration, pas de dépendances. Juste go test.
Table-driven tests : l'idiome Go
Les table-driven tests sont le pattern le plus utilisé en Go. Au lieu d'écrire une fonction par cas de test, vous définissez un tableau de cas :
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positifs", 2, 3, 5},
{"négatifs", -1, -2, -3},
{"zéro", 0, 0, 0},
{"mixte", -5, 10, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d, attendu %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
Chaque cas de test a un nom : quand un test échoue, vous savez immédiatement lequel. Et ajouter un nouveau cas ? Une seule ligne.
Sortie avec go test -v :
=== RUN TestAdd
=== RUN TestAdd/positifs
=== RUN TestAdd/négatifs
=== RUN TestAdd/zéro
=== RUN TestAdd/mixte
--- PASS: TestAdd (0.00s)
PASS
Benchmarks : mesurer les performances
Go intègre aussi les benchmarks. Même fichier _test.go, mais avec Benchmark au lieu de Test :
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
Lancez avec go test -bench=. :
$ go test -bench=.
BenchmarkAdd-8 1000000000 0.2850 ns/op
PASS
Go a exécuté la fonction 1 milliard de fois et chaque appel a pris 0.28 nanosecondes. C'est comme ça qu'on vérifie que le code est performant : avec des chiffres, pas des intuitions.
Vous pouvez aussi mesurer la mémoire avec -benchmem :
$ go test -bench=. -benchmem
BenchmarkAdd-8 1000000000 0.2850 ns/op 0 B/op 0 allocs/op
0 B/op : zéro allocation mémoire par opération. Voilà pourquoi Go est rapide.
Le piège du micro-benchmark. Une fonction aussi simple que Add est souvent inlinée par le compilateur, et comme son résultat n'est pas utilisé, l'appel peut être éliminé : on mesure alors presque le vide. Pour un benchmark fiable, consommez le résultat (en l'affectant à une variable de package) pour empêcher cette élimination. Depuis Go 1.24, la forme recommandée est for b.Loop() { … } plutôt que la boucle b.N.
Commandes utiles
# Lancer tous les tests
go test ./...
# Tests avec détails
go test -v ./...
# Couverture de code
go test -cover ./...
# Générer un rapport de couverture
go test -coverprofile=cover.out ./...
go tool cover -html=cover.out
# Benchmarks
go test -bench=. -benchmem ./...
go test ./... lance les tests dans tous les packages de votre projet. Le -cover vous montre quel pourcentage de votre code est couvert par les tests.
À vous d'essayer (les vrais tests tournent avec go test, mais voici la même logique en programme exécutable) :
package main
import "fmt"
func Add(a, b int) int {
return a + b
}
func main() {
cases := []struct {
name string
a, b int
expected int
}{
{"positifs", 2, 3, 5},
{"négatifs", -1, -2, -3},
{"zéro", 0, 0, 0},
}
for _, tt := range cases {
result := Add(tt.a, tt.b)
if result == tt.expected {
fmt.Printf("PASS %s: Add(%d, %d) = %d\n", tt.name, tt.a, tt.b, result)
} else {
fmt.Printf("FAIL %s: Add(%d, %d) = %d, attendu %d\n", tt.name, tt.a, tt.b, result, tt.expected)
}
}
}
The testing package
Go has testing built into the language. No need to install Jest, pytest, or PHPUnit. Everything is already there.
Simple rules:
- Test files are named
*_test.go(e.g.,math_test.go) - Test functions start with
Test(capital T) - They take a
*testing.Tparameter
In Go, how does go test know a function is a test? Is there an annotation or decorator to add, like in other languages? Before scrolling: what is the difference between t.Error and t.Fatal in a test?
See the answer
There is no annotation or decorator in Go. go test finds tests by convention: a test function must be named TestXxx (the word Test followed by a name starting with a capital letter) and take a single parameter t *testing.T, inside a file whose name ends with _test.go. The name and signature are enough; the tool discovers them automatically. To signal a failure: t.Error (or t.Errorf) marks the test as failed but continues executing the rest of the test function; t.Fatal (or t.Fatalf) marks the failure and stops the test function immediately, useful when the rest makes no sense (for example after an initialization error).
// math.go
package math
func Add(a, b int) int {
return a + b
}
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d, expected 5", result)
}
}
Run tests with go test:
$ go test
PASS
ok myproject/math 0.002s
0.002 seconds. No setup, no configuration, no dependencies. Just go test.
Table-driven tests — the Go idiom
Table-driven tests are the most common pattern in Go. Instead of writing one function per test case, you define a table of cases:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positives", 2, 3, 5},
{"negatives", -1, -2, -3},
{"zeros", 0, 0, 0},
{"mixed", -5, 10, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d, expected %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
Each test case has a name — when a test fails, you immediately know which one. And adding a new case? Just one line.
Output with go test -v:
=== RUN TestAdd
=== RUN TestAdd/positives
=== RUN TestAdd/negatives
=== RUN TestAdd/zeros
=== RUN TestAdd/mixed
--- PASS: TestAdd (0.00s)
PASS
Benchmarks: measuring performance
Go also has built-in benchmarks. Same _test.go file, but with Benchmark instead of Test:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
Run with go test -bench=.:
$ go test -bench=.
BenchmarkAdd-8 1000000000 0.2850 ns/op
PASS
Go executed the function 1 billion times and each call took 0.28 nanoseconds. That's how you verify code is performant — with numbers, not intuition.
You can also measure memory with -benchmem:
$ go test -bench=. -benchmem
BenchmarkAdd-8 1000000000 0.2850 ns/op 0 B/op 0 allocs/op
0 B/op — zero memory allocations per operation. That's why Go is fast.
Watch out for the micro-benchmark trap. A function as trivial as Add is often inlined by the compiler, and since its result isn't used, the call can be eliminated: you'd be measuring almost nothing. For a reliable benchmark, consume the result (assign it to a package-level variable) to prevent that elimination. Since Go 1.24, the recommended form is for b.Loop() { … } rather than the b.N loop.
Useful commands
# Run all tests
go test ./...
# Tests with details
go test -v ./...
# Code coverage
go test -cover ./...
# Generate coverage report
go test -coverprofile=cover.out ./...
go tool cover -html=cover.out
# Benchmarks
go test -bench=. -benchmem ./...
go test ./... runs tests in all packages of your project. The -cover flag shows what percentage of your code is covered by tests.
Try it yourself (real tests run with go test, but here is the same logic as a runnable program):
package main
import "fmt"
func Add(a, b int) int {
return a + b
}
func main() {
cases := []struct {
name string
a, b int
expected int
}{
{"positives", 2, 3, 5},
{"negatives", -1, -2, -3},
{"zero", 0, 0, 0},
}
for _, tt := range cases {
result := Add(tt.a, tt.b)
if result == tt.expected {
fmt.Printf("PASS %s: Add(%d, %d) = %d\n", tt.name, tt.a, tt.b, result)
} else {
fmt.Printf("FAIL %s: Add(%d, %d) = %d, expected %d\n", tt.name, tt.a, tt.b, result, tt.expected)
}
}
}
🎯 Pratique
S'entraîner (clique pour ouvrir) :
✨ Prompt IA
Copiez ce prompt dans Claude ou ChatGPT :
Écris des table-driven tests Go pour une fonction qui valide une adresse email. Teste les cas valides, invalides, vides et les cas limites. Ajoute un benchmark.
💬 Ré-explique sans regarder
Sans relire le code de l'IA : explique avec tes mots comment un table-driven test est structuré et pourquoi on nomme chaque cas via t.Run(tt.name, ...).
for _, tt := range tests. À l'intérieur, t.Run(tt.name, ...) crée un sous-test nommé : si un cas échoue, la sortie indique précisément lequel (ex : TestAdd/négatifs), et on peut relancer un seul cas. Ajouter un cas = ajouter une ligne au tableau.⚖️ Juge le code de l'IA
Tu as demandé à l'IA un test pour une fonction Divide(a, b int) (int, error). Voici ce qu'elle propose. Ton rôle de relecteur : l'accepter tel quel ou le rejeter, et dire pourquoi.
func TestDivide(t *testing.T) {
result, _ := Divide(10, 0)
if result != 0 {
t.Errorf("Divide(10, 0) = %d, attendu 0", result)
}
}
_ : pour une division par zéro, c'est justement l'error qu'on veut vérifier, pas la valeur de retour. Tel quel, le test passe même si Divide ne renvoie jamais d'erreur, donc il ne teste rien d'utile. Le bon réflexe : capturer l'erreur (result, err := Divide(10, 0)) et asserter if err == nil { t.Error("attendu une erreur pour la division par zéro") }.🧠 Rappel libre
Sans remonter dans la leçon : comment doit s'appeler un fichier de test en Go, et quelle commande lance les benchmarks (avec les allocations mémoire) ?
_test.go (ex : math_test.go) ; le suffixe _test est obligatoire pour que go test le détecte. Pour les benchmarks avec la mémoire : go test -bench=. -benchmem (le . signifie « tous les benchmarks », -benchmem ajoute les colonnes B/op et allocs/op).Pour aller plus loin
Vous maîtrisez maintenant les fondamentaux de Go. Voici les sujets avancés à explorer ensuite :
- Interfaces : le polymorphisme en Go, sans héritage Tour of Go
- Generics : les types paramétrés, arrivés avec Go 1.18 Tutorial
- Modules :
go.modpour gérer les dépendances Tutorial - Gin / Echo : frameworks web populaires pour les API Gin · Echo
- GORM : l'ORM le plus utilisé en Go gorm.io
Références complètes : Go.dev Documentation · A Tour of Go · Go by Example
Vous êtes un développeur polyvalent
Go est le langage de l'infrastructure moderne. Performant, simple, conçu pour le cloud. Avec HTML/CSS/JS pour le front, PHP ou Python pour le back, et Go pour les services haute performance, vous avez une palette complète de développeur polyvalent.
Vous n'êtes plus un débutant : vous êtes un développeur qui utilise l'IA comme un outil, pas comme une béquille.
En 6 leçons, vous avez appris les variables, les fonctions, la gestion d'erreurs, la concurrence, les API REST et les tests. Vous avez les bases pour construire des services rapides et fiables.
La suite ? Construisez quelque chose. Un CLI, une API, un microservice. Le meilleur apprentissage, c'est la pratique.
Going further
You now master Go's fundamentals. Here are advanced topics to explore next:
- Interfaces — polymorphism in Go, without inheritance Tour of Go
- Generics — parameterized types, introduced in Go 1.18 Tutorial
- Modules —
go.modfor dependency management Tutorial - Gin / Echo — popular web frameworks for APIs Gin · Echo
- GORM — the most popular ORM in Go gorm.io
Full references: Go.dev Documentation · A Tour of Go · Go by Example
You are a versatile developer
Go is the language of modern infrastructure. Performant, simple, designed for the cloud. With HTML/CSS/JS for the front, PHP or Python for the back, and Go for high-performance services, you have a complete versatile developer toolkit.
You're no longer a beginner — you're a developer who uses AI as a tool, not as a crutch.
In 6 lessons, you learned variables, functions, error handling, concurrency, REST APIs, and testing. You have the foundation to build fast and reliable services.
What's next? Build something. A CLI, an API, a microservice. The best learning is practice.
Vous maîtrisez maintenant Go de bout en bout : variables, erreurs, concurrence, API et tests. Le meilleur moyen d'ancrer tout ça, c'est de construire quelque chose de réel. Direction les projets pour mettre Go en pratique sur un cas concret.
Passer aux projets →