Pourquoi Tester Go avec Go Test ?
Tester est un aspect crucial dans le développement logiciel, et Go propose une solution native pour tester ses programmes, nommée Go Test. Ce processus de test aide à vérifier que votre code fonctionne correctement, permet une refactoring plus sûre, et contribue à la qualité globale du projet. Dans le contexte réel, un développeur doit tester chaque partie de son application pour s'assurer qu'elle répond aux exigences des utilisateurs et est robuste face aux anomalies.
Un cas d'utilisation concret serait de tester une fonction qui effectue un calcul mathématique complexe. Sans tests, il y aurait un risque significatif que le résultat soit incorrect ou que le code ne fonctionne pas comme prévu dans certaines conditions.
Prerequis
- Connaissance des bases de Go
- Installation de Go (version 1.16 ou ultérieure)
- Un éditeur de texte pour écrire et exécuter le code Go
Concepts fondamentaux
1. Structure d'un Test en Go
En Go, un test est simplement une fonction qui commence par Test suivi du nom de la fonction à tester. Elle prend un paramètre t *testing.T, qui contient des méthodes pour signaler les erreurs.
// Importation du package testing
import "testing"
// Fonction à tester
func Add(a, b int) int {
return a + b
}
// Test de la fonction Add
func TestAdd(t *testing.T) {
if result := Add(2, 3); result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}
2. Tableau de Tests
Les tests par tableau permettent de tester plusieurs cas d'usage en une seule fonction.
// Fonction à tester
func Multiply(a, b int) int {
return a * b
}
// Test de la fonction Multiply avec tableau de tests
func TestMultiply(t *testing.T) {
testCases := []struct {
a, b, expected int
}{
{2, 3, 6},
{-1, -1, 1},
{0, 5, 0},
}
for _, tc := range testCases {
if result := Multiply(tc.a, tc.b); result != tc.expected {
t.Errorf("Multiply(%d, %d) = %d; want %d", tc.a, tc.b, result, tc.expected)
}
}
}
3. Tests de Benchmarks
Les benchmarks permettent d'évaluer la performance de votre code.
// Fonction à benchmark
func Sum(numbers []int) int {
sum := 0
for _, num := range numbers {
sum += num
}
return sum
}
// Benchmark de la fonction Sum
func BenchmarkSum(b *testing.B) {
numbers := make([]int, 1000)
for i := range numbers {
numbers[i] = i
}
for n := 0; n < b.N; n++ {
Sum(numbers)
}
}
4. Tests de Benchmark Concurrents
Les benchmarks concurrents permettent d'évaluer la performance des fonctions sous charge.
// Fonction à benchmark concurrent
func ParallelSum(numbers []int) int {
sum := 0
for _, num := range numbers {
sum += num
}
return sum
}
// Benchmark concurrent de la fonction ParallelSum
func BenchmarkParallelSum(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
numbers := make([]int, 1000)
for i := range numbers {
numbers[i] = i
}
for pb.Next() {
ParallelSum(numbers)
}
})
}
5. Tests de Benchmark des Fonctions Répétitives
Les benchmarks répétitifs permettent d'évaluer la performance des fonctions avec un grand nombre d'itérations.
// Fonction à benchmark répétitive
func RepeatString(s string, n int) string {
result := ""
for i := 0; i < n; i++ {
result += s
}
return result
}
// Benchmark répétitif de la fonction RepeatString
func BenchmarkRepeatString(b *testing.B) {
b.Run("5", func(b *testing.B) {
b.ReportAllocs()
for n := 0; n < b.N; n++ {
RepeatString("a", 5)
}
})
b.Run("10", func(b *testing.B) {
b.ReportAllocs()
for n := 0; n < b.N; n++ {
RepeatString("a", 10)
}
})
}
Mise en pratique : projet fil rouge
Nous allons créer un simple gestionnaire de tâches (todo list) en Go. Le projet comprendra des fonctions pour ajouter, supprimer et afficher les tâches.
Étape 1 : Initialisation du Projet
mkdir todoapp
cd todoapp
go mod init todoapp
Étape 2 : Création des Fichiers
Créons trois fichiers : task.go, todo.go, et main_test.go.
task.go
// task.go
package main
import (
"errors"
)
type Task struct {
ID int
Desc string
}
var tasks = []Task{}
func GetTasks() []Task {
return tasks
}
func AddTask(desc string) (int, error) {
if desc == "" {
return 0, errors.New("Description cannot be empty")
}
id := len(tasks) + 1
task := Task{ID: id, Desc: desc}
tasks = append(tasks, task)
return id, nil
}
func RemoveTask(id int) error {
for i, task := range tasks {
if task.ID == id {
tasks = append(tasks[:i], tasks[i+1:]...)
return nil
}
}
return errors.New("Task not found")
}
todo.go
// todo.go
package main
import "fmt"
func PrintTasks() {
for _, task := range GetTasks() {
fmt.Printf("ID: %d, Description: %s\n", task.ID, task.Desc)
}
}
main_test.go
// main_test.go
package main
import (
"testing"
)
func TestAddTask(t *testing.T) {
_, err := AddTask("Buy groceries")
if err != nil {
t.Errorf("AddTask() error = %v; want nil", err)
}
tasks := GetTasks()
if len(tasks) != 1 {
t.Errorf("len(GetTasks()) = %d; want 1", len(tasks))
}
}
func TestRemoveTask(t *testing.T) {
id, _ := AddTask("Buy groceries")
RemoveTask(id)
tasks := GetTasks()
if len(tasks) != 0 {
t.Errorf("len(GetTasks()) = %d; want 0", len(tasks))
}
}
Étape 3 : Exécution des Tests
go test
Erreurs frequentes et debugging
Erreur :
undefined: AddTask# ❌ Mauvais _, err := AddTask("Buy groceries") if err != nil { t.Errorf("AddTask() error = %v; want nil", err) } # ✅ Correct id, _ := AddTask("Buy groceries") if id == 0 { t.Errorf("AddTask() returned ID = %d; want a valid ID", id) }Erreur :
len(GetTasks()) = 1; want 1# ❌ Mauvais tasks := GetTasks() if len(tasks) != 1 { t.Errorf("len(GetTasks()) = %d; want 1", len(tasks)) } # ✅ Correct tasks := GetTasks() if len(tasks) == 0 { t.Errorf("len(GetTasks()) = %d; want at least one task", len(tasks)) }Erreur :
RemoveTask() error = Task not found; want nil# ❌ Mauvais RemoveTask(1) tasks := GetTasks() if len(tasks) != 0 { t.Errorf("len(GetTasks()) = %d; want 0", len(tasks)) } # ✅ Correct id, _ := AddTask("Buy groceries") RemoveTask(id) tasks := GetTasks() if len(tasks) == 1 { t.Errorf("len(GetTasks()) = %d; want 0", len(tasks)) }
Pour aller plus loin
Test de Couverture : Apprenez à mesurer la couverture des tests avec
go test -cover.Tests Asynchrones : Découvrez comment tester des fonctions asynchrones en utilisant
t.Runpour créer des sous-tests.Mocking : Utilisez le package
mockerypour créer des mocks de dépendances lors de l'écriture de tests unitaires.
Défi pratique
Créez un test pour une fonction qui prend une liste de nombres et retourne la somme des cubes de ces nombres. Assurez-vous que vous couvrez tous les cas d'usage, y compris le cas où la liste est vide.