1. Introduction aux tests unitaires
Les tests unitaires consistent à vérifier et valider l'unité de test la plus petite dans un programme, telle qu'une fonction ou une méthode dans le langage Go. Les tests unitaires garantissent que le code fonctionne comme prévu et permettent aux développeurs d'apporter des modifications au code sans casser accidentellement les fonctionnalités existantes.
Dans un projet Golang, l'importance des tests unitaires va sans dire. Tout d'abord, ils peuvent améliorer la qualité du code, donnant aux développeurs plus de confiance pour apporter des changements au code. Deuxièmement, les tests unitaires peuvent servir de documentation pour le code, expliquant son comportement attendu. De plus, l'exécution automatique des tests unitaires dans un environnement d'intégration continue peut découvrir rapidement les bogues nouvellement introduits, améliorant ainsi la stabilité du logiciel.
2. Réalisation de tests de base à l'aide du package testing
La bibliothèque standard du langage Go comprend le package testing
, qui fournit des outils et des fonctionnalités pour écrire et exécuter des tests.
2.1 Création de votre premier cas de test
Pour écrire une fonction de test, vous devez créer un fichier avec le suffixe _test.go
. Par exemple, si votre fichier de code source est nommé calculator.go
, votre fichier de test doit être nommé calculator_test.go
.
Ensuite, il est temps de créer la fonction de test. Une fonction de test doit importer le package testing
et suivre un certain format. Voici un exemple simple :
// calculator_test.go
package calculator
import (
"testing"
"fmt"
)
// Tester la fonction d'addition
func TestAdd(t *testing.T) {
resultat := Add(1, 2)
attendu := 3
if resultat != attendu {
t.Errorf("Attendu %v, mais obtenu %v", attendu, resultat)
}
}
Dans cet exemple, TestAdd
est une fonction de test qui teste une fonction d'addition imaginaire. Si le résultat de la fonction Add
correspond au résultat attendu, le test réussira ; sinon, t.Errorf
sera appelé pour enregistrer les informations sur l'échec du test.
2.2 Comprendre les règles de dénomination et la signature des fonctions de test
Les fonctions de test doivent commencer par Test
, suivi de n'importe quelle chaîne non minuscule, et leur seul paramètre doit être un pointeur vers testing.T
. Comme le montre l'exemple, TestAdd
suit les règles de dénomination et la signature correcte.
2.3 Exécution des cas de test
Vous pouvez exécuter vos cas de test à l'aide de l'outil en ligne de commande. Pour un cas de test spécifique, exécutez la commande suivante :
go test -v // Exécute les tests dans le répertoire courant et affiche une sortie détaillée
Si vous souhaitez exécuter un cas de test spécifique, vous pouvez utiliser le drapeau -run
suivi d'une expression régulière :
go test -v -run TestAdd // Exécute uniquement la fonction de test TestAdd
La commande go test
trouvera automatiquement tous les fichiers _test.go
et exécutera chaque fonction de test qui répond aux critères. Si tous les tests réussissent, vous verrez un message similaire à PASS
dans la ligne de commande ; si un test échoue, vous verrez FAIL
accompagné du message d'erreur correspondant.
3. Écriture des cas de test
3.1 Signalement des erreurs à l'aide de t.Errorf
et t.Fatalf
En langage Go, le framework de tests fournit diverses méthodes pour signaler les erreurs. Les deux fonctions les plus couramment utilisées sont Errorf
et Fatalf
, toutes deux étant des méthodes de l'objet testing.T
. Errorf
est utilisé pour signaler les erreurs dans le test mais ne stoppe pas le cas de test en cours, tandis que Fatalf
arrête immédiatement le test en cours après avoir signalé une erreur. Il est important de choisir la méthode appropriée en fonction des exigences de test.
Exemple d'utilisation de Errorf
:
func TestAdd(t *testing.T) {
obtenu := Add(1, 2)
attendu := 3
if obtenu != attendu {
t.Errorf("Add(1, 2) = %d ; attendu %d", obtenu, attendu)
}
}
Si vous souhaitez arrêter immédiatement le test lorsqu'une erreur est détectée, vous pouvez utiliser Fatalf
:
func TestSubtract(t *testing.T) {
obtenu := Subtract(5, 3)
if obtenu != 2 {
t.Fatalf("Subtract(5, 3) = %d ; attendu 2", obtenu)
}
}
En général, si l'erreur entraînerait un dysfonctionnement du code ultérieur ou si l'échec du test peut être confirmé à l'avance, il est recommandé d'utiliser Fatalf
. Sinon, il est recommandé d'utiliser Errorf
pour obtenir un résultat de test plus complet.
3.2 Organisation des sous-tests et exécution des sous-tests
En Go, nous pouvons utiliser t.Run
pour organiser les sous-tests, ce qui nous aide à écrire du code de test de manière plus structurée. Les sous-tests peuvent avoir leur propre Setup
et Teardown
et peuvent être exécutés individuellement, offrant ainsi une grande flexibilité. C'est particulièrement utile pour réaliser des tests complexes ou des tests paramétrés.
Exemple d'utilisation du sous-test t.Run
:
func TestMultiply(t *testing.T) {
testcases := []struct {
name string
a, b, expected int
}{
{"2x3", 2, 3, 6},
{"-1x-1", -1, -1, 1},
{"0x4", 0, 4, 0},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
if got := Multiply(tc.a, tc.b); got != tc.expected {
t.Errorf("Multiply(%d, %d) = %d; attendu %d", tc.a, tc.b, got, tc.expected)
}
})
}
}
Si nous voulons exécuter le sous-test nommé "2x3" individuellement, nous pouvons exécuter la commande suivante dans la ligne de commande :
go test -run TestMultiply/2x3
Veuillez noter que les noms des sous-tests sont sensibles à la casse.
4. Travaux préparatoires avant et après les tests
4.1 Setup et Teardown
Lors de la réalisation de tests, nous devons souvent préparer un état initial pour les tests (comme la connexion à la base de données, la création de fichiers, etc.), et de même, nous devons effectuer un travail de nettoyage après la fin des tests. En Go, nous effectuons généralement Setup
et Teardown
directement dans les fonctions de test, et la fonction t.Cleanup
nous permet d'enregistrer des fonctions de rappel de nettoyage.
Voici un exemple simple :
func TestDatabase(t *testing.T) {
db, err := SetupDatabase()
if err != nil {
t.Fatalf("configuration échouée : %v", err)
}
// Enregistrer un rappel de nettoyage pour garantir que la connexion à la base de données est fermée lorsque le test est terminé
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Errorf("échec de la fermeture de la base de données : %v", err)
}
})
// Effectuer le test...
}
Dans la fonction TestDatabase
, nous appelons d'abord la fonction SetupDatabase
pour configurer l'environnement de test. Ensuite, nous utilisons t.Cleanup()
pour enregistrer une fonction qui sera appelée après la fin du test pour effectuer un travail de nettoyage, dans cet exemple, c'est la fermeture de la connexion à la base de données. De cette façon, nous pouvons nous assurer que les ressources sont libérées correctement, que le test réussisse ou échoue.
5. Amélioration de l'efficacité des tests
Améliorer l'efficacité des tests peut nous aider à itérer plus rapidement dans le développement, à découvrir rapidement les problèmes et à garantir la qualité du code. Ci-dessous, nous discuterons de la couverture des tests, des tests basés sur des tableaux et de l'utilisation de mocks pour améliorer l'efficacité des tests.
5.1 Couverture des tests et outils associés
L'outil go test
fournit une fonctionnalité très utile de couverture des tests, qui nous aide à comprendre quels parties du code sont couvertes par les cas de test, découvrant ainsi les parties du code qui ne sont pas couvertes par les tests.
En utilisant la commande go test -cover
, vous pouvez voir le pourcentage actuel de couverture des tests :
go test -cover
Si vous souhaitez avoir une compréhension plus détaillée des lignes de code exécutées et de celles qui ne le sont pas, vous pouvez utiliser le paramètre -coverprofile
, qui génère un fichier de données de couverture. Ensuite, vous pouvez utiliser la commande go tool cover
pour générer un rapport détaillé sur la couverture des tests.
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
La commande ci-dessus ouvrira un rapport web, montrant visuellement quelles lignes de code sont couvertes par les tests et lesquelles ne le sont pas. Le vert représente le code couvert par les tests, tandis que le rouge représente les lignes de code non couvertes.
5.2 Utilisation des Mocks
En matière de tests, nous rencontrons souvent des scénarios où nous devons simuler des dépendances externes. Les mocks peuvent nous aider à simuler ces dépendances, éliminant ainsi le besoin de compter sur des services externes spécifiques ou des ressources dans l'environnement de test.
Il existe de nombreux outils de mock dans la communauté Go, tels que testify/mock
et gomock
. Ces outils fournissent généralement une série d'API pour créer et utiliser des objets mock.
Voici un exemple basique d'utilisation de testify/mock
. La première chose à faire est de définir une interface et sa version mock :
type DataService interface {
FetchData() (int, error)
}
type MockDataService struct {
mock.Mock
}
func (m *MockDataService) FetchData() (int, error) {
args := m.Called()
return args.Int(0), args.Error(1)
}
En test, nous pouvons utiliser MockDataService
pour remplacer le service de données réel :
func TestSomething(t *testing.T) {
mockDataSvc := new(MockDataService)
mockDataSvc.On("FetchData").Return(42, nil) // Configuration du comportement attendu
result, err := mockDataSvc.FetchData() // Utilisation de l'objet mock
assert.NoError(t, err)
assert.Equal(t, 42, result)
mockDataSvc.AssertExpectations(t) // Vérification si le comportement attendu s'est produit
}
Grâce à cette approche, nous pouvons éviter de dépendre des services externes ou de mettre en isolation des appels de base de données, etc. dans les tests. Cela peut accélérer l'exécution des tests et rendre nos tests plus stables et fiables.
6. Techniques de Test Avancées
Après avoir maîtrisé les bases des tests unitaires en Go, nous pouvons explorer plus en détail certaines techniques de test avancées, qui aident à construire un logiciel plus robuste et à améliorer l'efficacité des tests.
6.1 Testing des Fonctions Privées
En Go, les fonctions privées font généralement référence à des fonctions non exportées, c'est-à-dire des fonctions dont les noms commencent par une lettre minuscule. En général, nous préférons tester les interfaces publiques, car elles reflètent l'utilité du code. Cependant, il existe des cas où tester directement les fonctions privées a également du sens, par exemple lorsque la fonction privée a une logique complexe et est appelée par plusieurs fonctions publiques.
Tester les fonctions privées diffère du test des fonctions publiques car elles ne peuvent pas être accédées depuis l'extérieur du package. Une technique courante consiste à écrire le code de test dans le même package, permettant ainsi l'accès à la fonction privée.
Voici un exemple simple :
// calculator.go
package calculator
func add(a, b int) int {
return a + b
}
Le fichier de test correspondant est le suivant :
// calculator_test.go
package calculator
import "testing"
func TestAdd(t *testing.T) {
expected := 4
actual := add(2, 2)
if actual != expected {
t.Errorf("attendu %d, obtenu %d", expected, actual)
}
}
En plaçant le fichier de test dans le même package, nous pouvons tester directement la fonction add
.
6.2 Modèles de Test Courants et Meilleures Pratiques
Les tests unitaires en Go bénéficient de certains modèles courants qui facilitent le travail de test et contribuent à maintenir la clarté et la maintenabilité du code.
-
Tests basés sur des tables
Les tests basés sur des tables sont une méthode d'organisation des entrées de test et des sorties attendues. En définissant un ensemble de cas de test, puis en les parcourant pour les tester, cette méthode rend très facile l'ajout de nouveaux cas de test et rend également le code plus facile à lire et à maintenir.
// calculator_test.go
package calculator
import "testing"
func TestAddTableDriven(t *testing.T) {
var tests = []struct {
a, b int
want int
}{
{1, 2, 3},
{2, 2, 4},
{5, -1, 4},
}
for _, tt := range tests {
testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
t.Run(testname, func(t *testing.T) {
ans := add(tt.a, tt.b)
if tt.want != ans {
t.Errorf("obtenu %d, attendu %d", ans, tt.want)
}
})
}
}
-
Utilisation de Mocks pour les Tests
Le mocking est une technique de test qui consiste à remplacer des dépendances pour tester différentes parties de la fonctionnalité. En Go, les interfaces sont la principale façon de mettre en œuvre des mocks. En utilisant des interfaces, une implémentation mock peut être créée puis utilisée en test.