Tests
Pour les développeurs, de bons tests permettent de détecter les erreurs dans le programme à l'avance, évitant ainsi la charge mentale causée par des bugs dus à une maintenance insuffisante. Il est donc très nécessaire de bien écrire les tests. Go fournit un outil de ligne de commande très pratique et utile go test pour les tests. On peut voir des tests dans la bibliothèque standard et dans de nombreux frameworks open source. Cet outil est très facile à utiliser et prend actuellement en charge les types de tests suivants :
- Tests d'exemple
- Tests unitaires
- Tests de référence (benchmark)
- Tests flous (fuzzing)
La plupart des API en Go sont fournies par la bibliothèque standard testing.
TIP
En exécutant la commande go help testfunc dans la ligne de commande, vous pouvez voir les explications de Go officielles pour les quatre types de tests ci-dessus.
Conventions de rédaction
Avant de commencer à écrire des tests, il faut d'abord prêter attention à quelques conventions, ce qui rendra l'apprentissage ultérieur plus facile.
- Package de test : Il est préférable de placer les fichiers de test dans un package séparé, généralement nommé
test. - Fichier de test : Les fichiers de test se terminent généralement par
_test.go. Par exemple, pour tester une fonction, nommez-lefunction_test.go. Si vous souhaitez diviser plus finement selon le type de test, vous pouvez également utiliser le type de test comme préfixe de fichier, par exemplebenchmark_marshaling_test.goouexample_marshaling_test.go. - Fonction de test : Chaque fichier de test contient plusieurs fonctions de test pour différents tests. Pour différents types de tests, le style de nommage des fonctions de test est également différent. Par exemple, les tests d'exemple sont
ExampleXXXX, les tests unitaires sontTestXXXX, les tests de référence sontBenchmarkXXXX, et les tests flous sontFuzzXXXX. Ainsi, même sans commentaires, on peut savoir de quel type de test il s'agit.
TIP
Lorsque le nom du package est testdata, ce package est généralement destiné à stocker des données auxiliaires pour les tests. Lors de l'exécution des tests, Go ignore les packages nommés testdata.
En suivant les conventions ci-dessus et en développant un bon style de test, vous pouvez éviter beaucoup de problèmes pour la maintenance future.
Exécution des tests
La commande go test est principalement utilisée pour exécuter des tests. Prenons un exemple de code réel. Supposons que nous ayons un fichier à tester /say/hello.go avec le code suivant :
package say
import "fmt"
func Hello() {
fmt.Println("hello")
}
func GoodBye() {
fmt.Println("bye")
}Et un fichier de test /test/example_test.go avec le code suivant :
package test
import (
"golearn/say"
)
func ExampleHello() {
say.Hello()
// Output:
// hello
}
func ExampleGoodBye() {
say.GoodBye()
// Output:
// bye
}
func ExampleSay() {
say.Hello()
say.GoodBye()
// Output:
// hello
// bye
}Il existe plusieurs façons d'exécuter ces tests. Par exemple, pour exécuter tous les cas de test du package test, vous pouvez exécuter la commande suivante directement dans le répertoire test :
$ go test ./
PASS
ok golearn/test 0.422s./ représente le répertoire actuel. Go recompilera tous les fichiers de test du répertoire test, puis exécutera tous les cas de test. D'après le résultat, on peut voir que tous les cas de test ont réussi. Les paramètres suivants peuvent également inclure plusieurs répertoires. Par exemple, la commande suivante montre que le répertoire principal du projet n'a pas de fichiers de test à exécuter.
$ go test ./ ../
ok golearn/test
? golearn [no test files]TIP
Lorsque les paramètres d'exécution incluent plusieurs packages, Go ne réexécute pas les cas de test qui ont déjà réussi. Lors de l'exécution, (cached) est ajouté à la fin de la ligne pour indiquer que le résultat de la sortie est issu du cache précédent. Go met en cache les résultats des tests lorsque les paramètres de test se trouvent dans l'ensemble suivant, sinon il ne les met pas en cache.
-benchtime, -cpu, -list, -parallel, -run, -short, -timeout, -failfast, -vSi vous souhaitez désactiver le cache, vous pouvez ajouter le paramètre -count=1.
Bien sûr, vous pouvez également spécifier un fichier de test particulier à exécuter.
$ go test example_test.go
ok command-line-arguments 0.457sOu vous pouvez spécifier un cas de test particulier dans un fichier de test, par exemple :
$ go test -run ExampleSay
PASS
ok golearn/test 0.038sBien que les trois situations ci-dessus aient toutes terminé les tests, les résultats de sortie sont trop concis. Dans ce cas, vous pouvez ajouter le paramètre -v pour rendre les résultats de sortie plus détaillés, par exemple :
$ go test ./ -v
=== RUN ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN ExampleGoodBye
--- PASS: ExampleGoodBye (0.00s)
=== RUN ExampleSay
--- PASS: ExampleSay (0.00s)
PASS
ok golearn/test 0.040sMaintenant, on peut voir clairement l'ordre d'exécution, le temps d'exécution, l'état d'exécution de chaque cas de test, ainsi que le temps total.
TIP
La commande go test exécute par défaut tous les tests unitaires, les tests d'exemple et les tests flous. Si vous ajoutez le paramètre -bench, elle exécutera tous les types de tests, par exemple la commande suivante :
$ go test -bench .Il est donc nécessaire d'utiliser le paramètre -run pour spécifier. Par exemple, la commande pour exécuter uniquement tous les tests de référence est la suivante :
$ go test -bench . -run ^$Paramètres courants
Les tests Go ont de nombreux paramètres. Seuls les paramètres courants sont présentés ci-dessous. Pour plus de détails, il est recommandé d'utiliser la commande go help testflag pour consulter.
| Paramètre | Description |
|---|---|
-o file | Spécifie le nom du fichier binaire après compilation |
-c | Compile uniquement les fichiers de test, sans les exécuter |
-json | Affiche les journaux de test au format JSON |
-exec xprog | Exécute les tests en utilisant xprog, équivalent à go run |
-bench regexp | Sélectionne les tests de référence correspondant à regexp |
-fuzz regexp | Sélectionne les tests flous correspondant à regexp |
-fuzztime t | Temps d'exécution automatique des tests flous, t est l'intervalle de temps |
-fuzzminimizetime t | Temps minimum d'exécution des tests de minimisation |
-count n | Exécute les tests n fois, par défaut 1 fois |
-cover | Active l'analyse de la couverture des tests |
-covermode set,count,atomic | Définit le mode d'analyse de la couverture |
-cpu | Exécute GOMAXPROCS pour les tests |
-failfast | Après le premier échec de test, ne démarre pas de nouveaux tests |
-list regexp | Liste les cas de test correspondant à regexp |
-parallel n | Permet l'exécution parallèle des tests appelant t.Parallel, n est le maximum |
-run regexp | Exécute uniquement les cas de test correspondant à regexp |
-skip regexp | Ignore les cas de test correspondant à regexp |
-timeout d | Si un test dépasse le temps d, il y a panic. d est un intervalle de temps |
-shuffle off,on,N | Mélange l'ordre d'exécution des tests, N est la graine aléatoire |
-v | Affiche des journaux de test plus détaillés |
-benchmem | Statistiques d'allocation de mémoire pour les tests de référence |
-blockprofile block.out | Statistiques de blocage des goroutines et écriture dans un fichier |
-blockprofilerate n | Contrôle la fréquence des statistiques de blocage |
-coverprofile cover.out | Statistiques de couverture des tests et écriture dans un fichier |
-cpuprofile cpu.out | Statistiques CPU et écriture dans un fichier |
-memprofile mem.out | Statistiques d'allocation de mémoire et écriture dans un fichier |
-memprofilerate n | Contrôle la fréquence des statistiques d'allocation de mémoire |
-mutexprofile mutex.out | Statistiques de contention de verrous et écriture dans un fichier |
-mutexprofilefraction n | Définit de statistiquer n goroutines en compétition pour un mutex |
-trace trace.out | Écrit les informations de suivi d'exécution dans un fichier |
-outputdir directory | Spécifie le répertoire de sortie des fichiers de statistiques |
Tests d'exemple
Les tests d'exemple ne visent pas à découvrir les problèmes du programme comme les trois autres types de tests. Ils servent plutôt à montrer comment utiliser une fonctionnalité et à servir de documentation. Les tests d'exemple ne sont pas un concept officiellement défini, ni une norme rigide, mais plutôt une convention d'ingénierie. Leur utilisation dépend des développeurs. Les tests d'exemple apparaissent très souvent dans la bibliothèque standard, généralement sous forme d'exemples de code écrits par l'équipe officielle. Par exemple, la fonction de test ExampleWithDeadline dans context/example_test.go de la bibliothèque standard montre comment utiliser DeadlineContext :
// This example passes a context with an arbitrary deadline to tell a blocking
// function that it should abandon its work as soon as it gets to it.
func ExampleWithDeadline() {
d := time.Now().Add(shortDuration)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its
// cancellation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
// Output:
// context deadline exceeded
}À première vue, cette fonction de test ressemble à une fonction ordinaire, mais les tests d'exemple sont principalement caractérisés par le commentaire Output. Lorsqu'une fonction à tester n'a qu'une seule ligne de sortie, utilisez le commentaire Output pour vérifier la sortie. Créez d'abord un fichier nommé hello.go avec le code suivant :
package say
import "fmt"
func Hello() {
fmt.Println("hello")
}
func GoodBye() {
fmt.Println("bye")
}La fonction SayHello est la fonction à tester. Ensuite, créez un fichier de test example_test.go avec le code suivant :
package test
import (
"golearn/say"
)
func ExampleHello() {
say.Hello()
// Output:
// hello
}
func ExampleGoodBye() {
say.GoodBye()
// Output:
// bye
}
func ExampleSay() {
say.Hello()
say.GoodBye()
// Output:
// hello
// bye
}Le commentaire Output dans la fonction indique que la sortie de la fonction doit être hello. Exécutons maintenant la commande de test pour voir le résultat.
$ go test -v
=== RUN ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN ExampleGoodBye
--- PASS: ExampleGoodBye (0.00s)
=== RUN ExampleSay
--- PASS: ExampleSay (0.00s)
PASS
ok golearn/test 0.448sD'après le résultat, on peut voir que tous les tests ont réussi. Concernant Output, il existe plusieurs façons de l'écrire. La première est une seule ligne de sortie, ce qui signifie vérifier si la sortie de la fonction est hello :
// Output:
// helloLa deuxième est plusieurs lignes de sortie, c'est-à-dire vérifier si la sortie correspond dans l'ordre :
// Output:
// hello
// byeLa troisième est une sortie non ordonnée, c'est-à-dire vérifier si plusieurs lignes de sortie correspondent sans ordre :
// Unordered output:
// bye
// helloIl est important de noter que pour une fonction de test, seules les dernières lignes qui sont des commentaires Output seront considérées comme un test d'exemple. Sinon, il s'agit simplement d'une fonction ordinaire qui ne sera pas exécutée par Go.
Tests unitaires
Les tests unitaires consistent à tester la plus petite unité testable d'un logiciel. La définition de la taille d'une unité dépend des développeurs. Cela peut être une structure, un package, une fonction ou un type. Continuons avec un exemple. Créez d'abord un fichier /tool/math.go avec le code suivant :
package tool
type Number interface {
~int8 | ~int16 | ~int32 | ~int64 | ~int
}
func SumInt[T Number](a, b T) T {
return a + b
}
func Equal[T Number](a, b T) bool {
return a == b
}Ensuite, créez un fichier de test /tool_test/unit_test.go. Pour les tests unitaires, le nom peut être unit_test ou utiliser le package ou la fonctionnalité à tester comme préfixe de fichier.
package test_test
import (
"golearn/tool"
"testing"
)
func TestSum(t *testing.T) {
a, b := 10, 101
expected := 111
actual := tool.SumInt(a, b)
if actual != expected {
t.Errorf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
}
func TestEqual(t *testing.T) {
a, b := 10, 101
expected := false
actual := tool.Equal(a, b)
if actual != expected {
t.Errorf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}Pour les tests unitaires, chaque cas de test doit être nommé selon le style TestXXXX, et le paramètre d'entrée de la fonction doit être t *testing.T. testing.T est une structure fournie par le package testing pour faciliter les tests, offrant de nombreuses méthodes utilisables. Dans l'exemple, t.Errorf est équivalent à t.Logf et est utilisé pour afficher les informations de journalisation des échecs de test de manière formatée. D'autres méthodes couramment utilisées incluent t.Fail pour marquer le cas actuel comme échoué. Une fonction similaire est t.FailNow qui marque également comme échoué, mais le premier continue l'exécution après l'échec, tandis que le second arrête directement l'exécution. Voici un exemple où le résultat attendu est modifié pour un résultat incorrect :
package tool_test
import (
"golearn/tool"
"testing"
)
func TestSum(t *testing.T) {
a, b := 10, 101
expected := 110
actual := tool.SumInt(a, b)
if actual != expected {
// Errorf utilise t.Fail() en interne
t.Errorf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
t.Log("test finished")
}
func TestEqual(t *testing.T) {
a, b := 10, 101
expected := true
actual := tool.Equal(a, b)
if actual != expected {
// Fatalf utilise t.FailNow() en interne
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
t.Log("test finished")
}L'exécution du test ci-dessus donne le résultat suivant :
$ go test tool_test.go -v
=== RUN TestSum
tool_test.go:14: Sum(10,101) expected 110,actual is 111
tool_test.go:16: test finished
--- FAIL: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
FAIL command-line-arguments 0.037sD'après les journaux de test, on peut voir que le cas TestSum a affiché "test finished" même s'il a échoué, tandis que TestEqual ne l'a pas fait. De même, t.SkipNow marque le cas actuel comme SKIP, puis arrête l'exécution. Il sera réexécuté lors du prochain tour de test.
package tool_test
import (
"golearn/tool"
"testing"
)
func TestSum(t *testing.T) {
a, b := 10, 101
expected := 110
actual := tool.SumInt(a, b)
if actual != expected {
t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
t.Log("test finished")
}
func TestEqual(t *testing.T) {
a, b := 10, 101
expected := true
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
t.Log("test finished")
}Lors de l'exécution du test, modifiez le nombre d'exécutions à 2 :
$ go test tool_test.go -v -count=2
=== RUN TestSum
tool_test.go:14: Sum(10,101) expected 110,actual is 111
--- SKIP: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
=== RUN TestSum
tool_test.go:14: Sum(10,101) expected 110,actual is 111
--- SKIP: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
FAIL command-line-arguments 0.468sDans l'exemple ci-dessus, "test finished" est affiché à la dernière ligne pour indiquer la fin du test. En fait, vous pouvez utiliser t.Cleanup pour enregistrer une fonction de nettoyage qui sera exécutée à la fin du cas de test, comme suit :
package tool_test
import (
"golearn/tool"
"testing"
)
func finished(t *testing.T) {
t.Log("test finished")
}
func TestSum(t *testing.T) {
t.Cleanup(func() {
finished(t)
})
a, b := 10, 101
expected := 111
actual := tool.SumInt(a, b)
if actual != expected {
t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
}
func TestEqual(t *testing.T) {
t.Cleanup(func() {
finished(t)
})
a, b := 10, 101
expected := false
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}L'exécution du test donne le résultat suivant :
$ go test tool_test.go -v
=== RUN TestSum
tool_test.go:9: test finished
--- PASS: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:9: test finished
--- PASS: TestEqual (0.00s)
PASS
ok command-line-arguments 0.462sHelper
t.Helper() permet de marquer la fonction actuelle comme une fonction d'aide. Une fonction d'aide n'est pas exécutée comme un cas de test séparé. Lors de l'enregistrement des journaux, le numéro de ligne affiché est celui de l'appelant de la fonction d'aide. Cela permet un positionnement plus précis lors de l'analyse des journaux et évite les informations redondantes. Par exemple, l'exemple t.Cleanup ci-dessus peut être modifié en une fonction d'aide comme suit :
package tool_test
import (
"golearn/tool"
"testing"
)
func CleanupHelper(t *testing.T) {
t.Helper()
t.Log("test finished")
}
func TestSum(t *testing.T) {
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := 111
actual := tool.SumInt(a, b)
if actual != expected {
t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
}
func TestEqual(t *testing.T) {
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := false
t.Helper()
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}L'exécution du test donne les informations suivantes. La différence par rapport à avant est que le numéro de ligne de "test finished" est devenu celui de l'appelant.
$ go test tool_test.go -v
=== RUN TestSum
tool_test.go:15: test finished
--- PASS: TestSum (0.00s)
=== RUN TestEqual
tool_test.go:30: test finished
--- PASS: TestEqual (0.00s)
PASS
ok command-line-arguments 0.464sTIP
Les opérations ci-dessus ne peuvent être effectuées que dans le test principal, c'est-à-dire les cas de test exécutés directement. Si elles sont utilisées dans un sous-test, cela provoquera un panic.
Sous-tests
Dans certains cas, il peut être nécessaire de tester d'autres cas de test dans un cas de test. Ces cas de test imbriqués sont généralement appelés sous-tests. La méthode t.Run() permet de le faire. Sa signature est la suivante :
// La méthode Run démarre une nouvelle goroutine pour exécuter le sous-test
// Elle bloque jusqu'à ce que la fonction f soit terminée
// La valeur de retour indique si le test a réussi
func (t *T) Run(name string, f func(t *T)) boolVoici un exemple :
func TestTool(t *testing.T) {
t.Run("tool.Sum(10,101)", TestSum)
t.Run("tool.Equal(10,101)", TestEqual)
}Le résultat de l'exécution est le suivant :
$ go test -run TestTool -v
=== RUN TestTool
=== RUN TestTool/tool.Sum(10,101)
tool_test.go:15: test finished
=== RUN TestTool/tool.Equal(10,101)
tool_test.go:30: test finished
--- PASS: TestTool (0.00s)
--- PASS: TestTool/tool.Sum(10,101) (0.00s)
--- PASS: TestTool/tool.Equal(10,101) (0.00s)
PASS
ok golearn/tool_test 0.449sD'après la sortie, on peut voir clairement la structure hiérarchique parent-enfant. Dans l'exemple ci-dessus, le deuxième sous-test ne sera pas exécuté tant que le premier n'est pas terminé. Vous pouvez utiliser t.Parallel() pour marquer les cas de test comme pouvant être exécutés en parallèle. Dans ce cas, l'ordre de sortie ne sera pas déterminé.
package tool_test
import (
"golearn/tool"
"testing"
)
func CleanupHelper(t *testing.T) {
t.Helper()
t.Log("test finished")
}
func TestSum(t *testing.T) {
t.Parallel()
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := 111
actual := tool.SumInt(a, b)
if actual != expected {
t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
}
}
func TestEqual(t *testing.T) {
t.Parallel()
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := false
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}
func TestToolParallel(t *testing.T) {
t.Log("setup")
t.Run("tool.Sum(10,101)", TestSum)
t.Run("tool.Equal(10,101)", TestEqual)
t.Log("teardown")
}L'exécution du test donne le résultat suivant :
$ go test -run TestTool -v
=== RUN TestToolParallel
tool_test.go:46: setup
=== RUN TestToolParallel/tool.Sum(10,101)
=== PAUSE TestToolParallel/tool.Sum(10,101)
=== RUN TestToolParallel/tool.Equal(10,101)
=== PAUSE TestToolParallel/tool.Equal(10,101)
=== NAME TestToolParallel
tool_test.go:49: teardown
=== CONT TestToolParallel/tool.Sum(10,101)
=== CONT TestToolParallel/tool.Equal(10,101)
=== NAME TestToolParallel/tool.Sum(10,101)
tool_test.go:16: test finished
=== NAME TestToolParallel/tool.Equal(10,101)
tool_test.go:32: test finished
--- PASS: TestToolParallel (0.00s)
--- PASS: TestToolParallel/tool.Sum(10,101) (0.00s)
--- PASS: TestToolParallel/tool.Equal(10,101) (0.00s)
PASS
ok golearn/tool_test 0.444sD'après les résultats du test, on peut voir clairement qu'il y a un processus d'attente de blocage. Lors de l'exécution concurrente des cas de test, l'exemple ci-dessus ne peut pas être exécuté normalement car le code suivant ne peut pas garantir une exécution synchrone. Dans ce cas, vous pouvez choisir d'imbriquer une autre couche de t.Run(), comme suit :
func TestToolParallel(t *testing.T) {
t.Log("setup")
t.Run("process", func(t *testing.T) {
t.Run("tool.Sum(10,101)", TestSum)
t.Run("tool.Equal(10,101)", TestEqual)
})
t.Log("teardown")
}En exécutant à nouveau, vous pouvez voir le résultat d'exécution normal.
$ go test -run TestTool -v
=== RUN TestToolParallel
tool_test.go:46: setup
=== RUN TestToolParallel/process
=== RUN TestToolParallel/process/tool.Sum(10,101)
=== PAUSE TestToolParallel/process/tool.Sum(10,101)
=== RUN TestToolParallel/process/tool.Equal(10,101)
=== PAUSE TestToolParallel/process/tool.Equal(10,101)
=== CONT TestToolParallel/process/tool.Sum(10,101)
=== CONT TestToolParallel/process/tool.Equal(10,101)
=== NAME TestToolParallel/process/tool.Sum(10,101)
tool_test.go:16: test finished
=== NAME TestToolParallel/process/tool.Equal(10,101)
tool_test.go:32: test finished
=== NAME TestToolParallel
tool_test.go:51: teardown
--- PASS: TestToolParallel (0.00s)
--- PASS: TestToolParallel/process (0.00s)
--- PASS: TestToolParallel/process/tool.Sum(10,101) (0.00s)
--- PASS: TestToolParallel/process/tool.Equal(10,101) (0.00s)
PASS
ok golearn/tool_test 0.450sStyle de tableau
Dans les tests unitaires ci-dessus, les données d'entrée des tests sont des variables déclarées manuellement une par une. Lorsque la quantité de données est faible, cela n'a pas d'importance. Mais si vous souhaitez tester plusieurs ensembles de données, il n'est pas possible de déclarer des variables pour créer des données de test. Par conséquent, il est généralement recommandé d'utiliser une forme de slice de structures. Les structures sont des structures anonymes déclarées temporairement. Comme ce style de codage ressemble à un tableau, il est appelé table-driven. Voici un exemple. Il s'agit d'un exemple où plusieurs variables sont déclarées manuellement pour créer des données de test. S'il y a plusieurs ensembles de données, cela ne semble pas très intuitif. Modifions-le donc en style de tableau :
func TestEqual(t *testing.T) {
t.Cleanup(func() {
CleanupHelper(t)
})
a, b := 10, 101
expected := false
actual := tool.Equal(a, b)
if actual != expected {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
}
}Le code modifié est le suivant :
func TestEqual(t *testing.T) {
t.Cleanup(func() {
CleanupHelper(t)
})
// style table driven
testData := []struct {
a, b int
exp bool
}{
{10, 101, false},
{5, 5, true},
{30, 32, false},
{100, 101, false},
{2, 3, false},
{4, 4, true},
}
for _, data := range testData {
if actual := tool.Equal(data.a, data.b); actual != data.exp {
t.Fatalf("Sum(%d,%d) expected %t,actual is %t", data.a, data.b, data.exp, actual)
}
}
}Ces données de test semblent beaucoup plus intuitives.
Tests de référence (Benchmark)
Les tests de référence, également appelés tests de performance, sont généralement utilisés pour tester les indicateurs de performance tels que l'occupation de la mémoire, l'utilisation du CPU, le temps d'exécution, etc. Pour les tests de référence, les fichiers de test se terminent généralement par bench_test.go et les fonctions des cas de test doivent être au format BenchmarkXXXX.
Prenons l'exemple d'une comparaison de performance de concaténation de chaînes. Comme chacun le sait, utiliser directement + pour concaténer des chaînes est très peu performant, tandis que l'utilisation de strings.Builder est bien meilleure. Créez un fichier /tool/strings.go avec deux fonctions pour effectuer la concaténation de chaînes de deux manières :
package tool
import "strings"
func ConcatStringDirect(longString string) {
res := ""
for i := 0; i < 100_000; i++ {
res += longString
}
}
func ConcatStringWithBuilder(longString string) {
var res strings.Builder
for i := 0; i < 100_000; i++ {
res.WriteString(longString)
}
}Ensuite, créez un fichier de test /tool_test/bench_tool_test.go avec le code suivant :
package tool_test
import (
"golearn/tool"
"testing"
)
var longString = "longStringlongStringlongStringlongStringlongStringlongStringlongStringlongString"
func BenchmarkConcatDirect(b *testing.B) {
for i := 0; i < b.N; i++ {
tool.ConcatStringDirect(longString)
}
}
func BenchmarkConcatBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
tool.ConcatStringWithBuilder(longString)
}
}Exécutez la commande de test. La commande active les journaux détaillés et l'analyse de la mémoire, spécifie la liste des cœurs CPU utilisés, et chaque cas de test est exécuté deux fois. Le résultat est le suivant :
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=2
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2 4 277771375 ns/op 4040056736 B/op 10000 allocs/op
BenchmarkConcatDirect-2 4 278500125 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-4 1 1153796000 ns/op 4040068784 B/op 10126 allocs/op
BenchmarkConcatDirect-4 1 1211017600 ns/op 4040073104 B/op 10171 allocs/op
BenchmarkConcatDirect-8 2 665460800 ns/op 4040077760 B/op 10219 allocs/op
BenchmarkConcatDirect-8 2 679774450 ns/op 4040080064 B/op 10243 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2 3428 344530 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 3579 351858 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-4 2448 736177 ns/op 4128185 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1688 662993 ns/op 4128185 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1958 550333 ns/op 4128199 B/op 29 allocs/op
BenchmarkConcatBuilder-8 2174 552113 ns/op 4128196 B/op 29 allocs/op
PASS
ok golearn/tool_test 21.381sVoici une explication des résultats de sortie des tests de référence. goos représente le système d'exploitation en cours d'exécution, goarch représente l'architecture du CPU, pkg est le package où se trouve le test, et cpu contient des informations sur le CPU. Les résultats de chaque cas de test sont séparés par le nom de chaque test de référence. Le 2 dans la première colonne BenchmarkConcatDirect-2 représente le nombre de cœurs CPU utilisés. Le 4 dans la deuxième colonne représente la valeur de b.N dans le code, c'est-à-dire le nombre de boucles dans le test de référence. La troisième colonne 277771375 ns/op représente le temps consommé pour chaque boucle, où ns est la nanoseconde. La quatrième colonne 4040056736 B/op représente la taille en octets de la mémoire allouée pour chaque boucle. La cinquième colonne 10000 allocs/op représente le nombre d'allocations de mémoire pour chaque boucle.
Il est évident, d'après les résultats du test, que l'utilisation de strings.Builder est bien plus performante que l'utilisation de + pour concaténer des chaînes. La comparaison des performances via des données intuitives est l'objectif des tests de référence.
benchstat
benchstat est un outil open source d'analyse de tests de performance. Dans les tests de performance ci-dessus, il n'y a que deux échantillons. Une fois que le nombre d'échantillons augmente, l'analyse manuelle devient très fastidieuse et chronophage. Cet outil est né pour résoudre les problèmes d'analyse de performance.
Il faut d'abord télécharger l'outil :
$ go install golang.org/x/perf/benchstatExécutez les tests de référence deux fois. Cette fois, modifiez le nombre d'échantillons à 5 et sortez-les respectivement dans les fichiers old.txt et new.txt pour comparaison. Premier résultat d'exécution :
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=5 | tee -a old.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2 4 290535650 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 298974625 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 299637800 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 276487000 ns/op 4040056784 B/op 10001 allocs/op
BenchmarkConcatDirect-2 4 356465275 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-4 2 894723200 ns/op 4040077424 B/op 10216 allocs/op
BenchmarkConcatDirect-4 2 785830400 ns/op 4040078288 B/op 10225 allocs/op
BenchmarkConcatDirect-4 2 743634000 ns/op 4040077568 B/op 10217 allocs/op
BenchmarkConcatDirect-4 2 953802700 ns/op 4040075408 B/op 10195 allocs/op
BenchmarkConcatDirect-4 2 953028750 ns/op 4040077520 B/op 10217 allocs/op
BenchmarkConcatDirect-8 2 684023150 ns/op 4040086784 B/op 10313 allocs/op
BenchmarkConcatDirect-8 2 634380250 ns/op 4040090528 B/op 10352 allocs/op
BenchmarkConcatDirect-8 2 685030600 ns/op 4040090768 B/op 10355 allocs/op
BenchmarkConcatDirect-8 2 817909650 ns/op 4040089808 B/op 10345 allocs/op
BenchmarkConcatDirect-8 2 600078100 ns/op 4040095664 B/op 10406 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2 2925 419651 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 2961 423899 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 2714 422275 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 2848 452255 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 2612 454452 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-4 974 1158000 ns/op 4128189 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1098 1068682 ns/op 4128192 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1042 1056570 ns/op 4128194 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1280 978213 ns/op 4128191 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1538 1162108 ns/op 4128190 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1744 700824 ns/op 4128203 B/op 29 allocs/op
BenchmarkConcatBuilder-8 2235 759537 ns/op 4128201 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1556 736455 ns/op 4128204 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1592 825794 ns/op 4128201 B/op 29 allocs/op
BenchmarkConcatBuilder-8 2263 717285 ns/op 4128203 B/op 29 allocs/op
PASS
ok golearn/tool_test 56.742sDeuxième résultat d'exécution :
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=5 | tee -a new.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2 4 285074900 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 291517150 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 281901975 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 292320625 ns/op 4040056592 B/op 9999 allocs/op
BenchmarkConcatDirect-2 4 286723000 ns/op 4040056952 B/op 10002 allocs/op
BenchmarkConcatDirect-4 1 1188983000 ns/op 4040071856 B/op 10158 allocs/op
BenchmarkConcatDirect-4 1 1080713900 ns/op 4040070800 B/op 10147 allocs/op
BenchmarkConcatDirect-4 1 1203622300 ns/op 4040067344 B/op 10111 allocs/op
BenchmarkConcatDirect-4 1 1045291300 ns/op 4040070224 B/op 10141 allocs/op
BenchmarkConcatDirect-4 1 1123163300 ns/op 4040070032 B/op 10139 allocs/op
BenchmarkConcatDirect-8 2 790421300 ns/op 4040076656 B/op 10208 allocs/op
BenchmarkConcatDirect-8 2 659047300 ns/op 4040079488 B/op 10237 allocs/op
BenchmarkConcatDirect-8 2 712991800 ns/op 4040077184 B/op 10213 allocs/op
BenchmarkConcatDirect-8 2 706605350 ns/op 4040078000 B/op 10222 allocs/op
BenchmarkConcatDirect-8 2 656195700 ns/op 4040085248 B/op 10297 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2 2726 386412 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 3439 335358 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 3376 338957 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 3870 326301 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-2 4285 339596 ns/op 4128176 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1663 671535 ns/op 4128187 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1507 744885 ns/op 4128191 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1353 1097800 ns/op 4128187 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1388 1006019 ns/op 4128189 B/op 29 allocs/op
BenchmarkConcatBuilder-4 1635 993764 ns/op 4128189 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1332 783599 ns/op 4128198 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1818 729821 ns/op 4128202 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1398 780614 ns/op 4128202 B/op 29 allocs/op
BenchmarkConcatBuilder-8 1526 750513 ns/op 4128204 B/op 29 allocs/op
BenchmarkConcatBuilder-8 2164 704798 ns/op 4128204 B/op 29 allocs/op
PASS
ok golearn/tool_test 50.387sEnsuite, utilisez benchstat pour comparer :
$ benchstat old.txt new.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
│ old.txt │ new.txt │
│ sec/op │ sec/op vs base │
ConcatDirect-2 299.0m ± ∞ ¹ 286.7m ± ∞ ¹ ~ (p=0.310 n=5)
ConcatDirect-4 894.7m ± ∞ ¹ 1123.2m ± ∞ ¹ +25.53% (p=0.008 n=5)
ConcatDirect-8 684.0m ± ∞ ¹ 706.6m ± ∞ ¹ ~ (p=0.548 n=5)
ConcatBuilder-2 423.9µ ± ∞ ¹ 339.0µ ± ∞ ¹ -20.04% (p=0.008 n=5)
ConcatBuilder-4 1068.7µ ± ∞ ¹ 993.8µ ± ∞ ¹ ~ (p=0.151 n=5)
ConcatBuilder-8 736.5µ ± ∞ ¹ 750.5µ ± ∞ ¹ ~ (p=0.841 n=5)
geomean 19.84m 19.65m -0.98%
¹ need >= 6 samples for confidence interval at level 0.95
│ old.txt │ new.txt │
│ B/op │ B/op vs base │
ConcatDirect-2 3.763Gi ± ∞ ¹ 3.763Gi ± ∞ ¹ ~ (p=1.000 n=5)
ConcatDirect-4 3.763Gi ± ∞ ¹ 3.763Gi ± ∞ ¹ -0.00% (p=0.008 n=5)
ConcatDirect-8 3.763Gi ± ∞ ¹ 3.763Gi ± ∞ ¹ -0.00% (p=0.008 n=5)
ConcatBuilder-2 3.937Mi ± ∞ ¹ 3.937Mi ± ∞ ¹ ~ (p=1.000 n=5) ²
ConcatBuilder-4 3.937Mi ± ∞ ¹ 3.937Mi ± ∞ ¹ ~ (p=0.079 n=5)
ConcatBuilder-8 3.937Mi ± ∞ ¹ 3.937Mi ± ∞ ¹ ~ (p=0.952 n=5)
geomean 123.2Mi 123.2Mi -0.00%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal
│ old.txt │ new.txt │
│ allocs/op │ allocs/op vs base │
ConcatDirect-2 9.999k ± ∞ ¹ 9.999k ± ∞ ¹ ~ (p=1.000 n=5)
ConcatDirect-4 10.22k ± ∞ ¹ 10.14k ± ∞ ¹ -0.74% (p=0.008 n=5)
ConcatDirect-8 10.35k ± ∞ ¹ 10.22k ± ∞ ¹ -1.26% (p=0.008 n=5)
ConcatBuilder-2 29.00 ± ∞ ¹ 29.00 ± ∞ ¹ ~ (p=1.000 n=5) ²
ConcatBuilder-4 29.00 ± ∞ ¹ 29.00 ± ∞ ¹ ~ (p=1.000 n=5) ²
ConcatBuilder-8 29.00 ± ∞ ¹ 29.00 ± ∞ ¹ ~ (p=1.000 n=5) ²
geomean 543.6 541.7 -0.33%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equalD'après les résultats, on peut voir que benchstat les divise en trois groupes : le temps d'exécution, l'occupation de la mémoire et le nombre d'allocations de mémoire. Parmi eux, geomean est la moyenne géométrique, p est le niveau de signification de l'échantillon. La zone critique est généralement de 0,05. Au-dessus de 0,05, ce n'est pas très fiable. Prenons l'une des données comme exemple :
│ sec/op │ sec/op vs base │
ConcatDirect-4 894.7m ± ∞ ¹ 1123.2m ± ∞ ¹ +25.53% (p=0.008 n=5)On peut voir que le temps d'exécution de old est de 894,7 ms, celui de new est de 1123,2 ms, ce qui représente une augmentation de 25,53% du temps d'exécution.
Tests flous (Fuzzing)
Le test flou est une nouvelle fonctionnalité introduite dans Go 1.18. C'est une amélioration des tests unitaires et des tests de référence. La différence est que les données de test des deux premiers doivent être écrites manuellement par les développeurs, tandis que les tests flous peuvent générer des données de test aléatoires via un corpus. Pour en savoir plus sur les tests flous en Go, vous pouvez consulter Go Fuzzing. L'avantage des tests flous est que, par rapport aux données de test fixes, les données aléatoires peuvent mieux tester les conditions aux limites du programme. Prenons l'exemple du tutoriel officiel. Cette fois, nous devons tester une fonction d'inversion de chaîne. Créez d'abord un fichier /tool/strings.go avec le code suivant :
package tool
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}Créez un fichier de test flou /tool_test/fuzz_tool_test.go avec le code suivant :
package tool
import (
"golearn/tool"
"testing"
"unicode/utf8"
)
func FuzzReverse(f *testing.F) {
testdata := []string{"hello world!", "nice to meet you", "good bye!"}
for _, data := range testdata {
f.Add(data)
}
f.Fuzz(func(t *testing.T, str string) {
first := tool.Reverse(str)
second := tool.Reverse(first)
t.Logf("str:%q,first:%q,second:%q", str, first, second)
if str != second {
t.Errorf("before: %q, after: %q", str, second)
}
if utf8.ValidString(str) && !utf8.ValidString(first) {
t.Errorf("Reverse produced invalid UTF-8 string %q %q", str, first)
}
})
}Dans les tests flous, il faut d'abord ajouter des données au corpus de graines. Dans l'exemple, on utilise f.Add() pour ajouter, ce qui aide à générer des données de test aléatoires par la suite. Ensuite, utilisez f.Fuzz(fn) pour effectuer le test. La signature de la fonction est la suivante :
func (f *F) Fuzz(ff any)
func (f *F) Add(args ...any)fn est similaire à la logique d'une fonction de test unitaire. Le premier paramètre d'entrée de la fonction doit être t *testing.T, suivi des paramètres à générer. Comme les chaînes transmises sont imprévisibles, on utilise ici la méthode d'inversion deux fois pour vérifier. Exécutez la commande suivante :
$ go test -run Fuzz -v
=== RUN FuzzReverse
=== RUN FuzzReverse/seed#0
fuzz_tool_test.go:18: str:"hello world!",first:"!dlrow olleh",second:"hello world!"
=== RUN FuzzReverse/seed#1
fuzz_tool_test.go:18: str:"nice to meet you",first:"uoy teem ot ecin",second:"nice to meet you"
=== RUN FuzzReverse/seed#2
fuzz_tool_test.go:18: str:"good bye!",first:"!eyb doog",second:"good bye!"
--- PASS: FuzzReverse (0.00s)
--- PASS: FuzzReverse/seed#0 (0.00s)
--- PASS: FuzzReverse/seed#1 (0.00s)
--- PASS: FuzzReverse/seed#2 (0.00s)
PASS
ok golearn/tool_test 0.539sLorsque le paramètre ne contient pas -fuzz, aucune donnée de test aléatoire ne sera générée. Seules les données du corpus seront transmises à la fonction de test. D'après les résultats, on peut voir que tous les tests ont réussi. Ainsi utilisé, cela équivaut à un test unitaire, mais il y a en fait un problème. Ajoutons le paramètre -fuzz et exécutons à nouveau :
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/217 completed
fuzz: minimizing 91-byte failing input file
fuzz: elapsed: 0s, gathering baseline coverage: 15/217 completed
--- FAIL: FuzzReverse (0.13s)
--- FAIL: FuzzReverse (0.00s)
fuzz_tool_test.go:18: str:"𐑄",first:"\x84\x91\x90\xf0",second:"𐑄"
fuzz_tool_test.go:23: Reverse produced invalid UTF-8 string "𐑄" "\x84\x91\x90\xf0"
Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
To re-run:
go test -run=FuzzReverse/d856c981b6266ba2
=== NAME
FAIL
exit status 1
FAIL golearn/tool_test 0.697sTIP
Les cas de test qui échouent lors des tests flous sont écrits dans un fichier de corpus sous le répertoire testdata du dossier de test actuel, par exemple dans l'exemple ci-dessus :
Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
To re-run:
go test -run=FuzzReverse/d856c981b6266ba2testdata\fuzz\FuzzReverse\d856c981b6266ba2 est le chemin du fichier de corpus de sortie, dont le contenu est le suivant :
go test fuzz v1
string("𐑄")On peut voir que cette fois le test n'a pas réussi, car la chaîne inversée est devenue un format non-UTF8. Ainsi, le test flou a permis de découvrir ce problème. Comme certains caractères occupent plus d'un octet, si on les inverse octet par octet, cela donnera certainement du charabia. Modifions donc le code source à tester comme suit, en convertissant la chaîne en []rune, ce qui permettra d'éviter le problème ci-dessus :
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}Ensuite, exécutez directement le cas de test qui a échoué lors du test flou précédent :
$ go test -run=FuzzReverse/d856c981b6266ba2 -v
=== RUN FuzzReverse
=== RUN FuzzReverse/d856c981b6266ba2
fuzz_tool_test.go:18: str:"𐑄",first:"𐑄",second:"𐑄"
--- PASS: FuzzReverse (0.00s)
--- PASS: FuzzReverse/d856c981b6266ba2 (0.00s)
PASS
ok golearn/tool_test 0.033sOn peut voir que cette fois le test a réussi. Exécutons à nouveau le test flou pour voir s'il y a d'autres problèmes :
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/219 completed
fuzz: minimizing 70-byte failing input file
failure while testing seed corpus entry: FuzzReverse/d97214ce235bfcf5
fuzz: elapsed: 0s, gathering baseline coverage: 2/219 completed
--- FAIL: FuzzReverse (0.15s)
--- FAIL: FuzzReverse (0.00s)
fuzz_tool_test.go:18: str:"\xe4",first:"",second:""
fuzz_tool_test.go:20: before: "\xe4", after: ""
=== NAME
FAIL
exit status 1
FAIL golearn/tool_test 0.184sOn peut voir qu'il y a à nouveau une erreur. Cette fois, le problème est que la chaîne n'est pas égale après deux inversions. Le caractère original est \xe4, le résultat attendu est \xe4, mais le résultat est du charabia, comme suit :
func main() {
fmt.Println("\xe4")
fmt.Println([]byte("\xe4"))
fmt.Println([]rune("\xe4"))
fmt.Printf("%q\n", "\xe4")
fmt.Printf("%x\n", "\xe4")
}Le résultat de l'exécution est :
[65533]
"\xe4"
e4La raison en est que \xe4 représente un octet, mais ce n'est pas une séquence UTF-8 valide (dans l'encodage UTF-8, \xe4 est le début d'un caractère à trois octets, mais il manque les deux octets suivants). Lors de la conversion en []rune, Golang le transforme automatiquement en un caractère Unicode unique []rune{"\uFFFD"}, qui après inversion reste []rune{"\uFFFD"}. Lors de la reconversion en string, ce caractère Unicode est à nouveau remplacé par son encodage UTF-8 \xef\xbf\xbd. Une solution consiste donc à retourner directement une erreur si la chaîne transmise n'est pas UTF-8 :
func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil
}Le code de test doit également être légèrement modifié :
func FuzzReverse(f *testing.F) {
testdata := []string{"hello world!", "nice to meet you", "good bye!"}
for _, data := range testdata {
f.Add(data)
}
f.Fuzz(func(t *testing.T, str string) {
first, err := tool.Reverse(str)
if err != nil {
t.Skip()
}
second, err := tool.Reverse(first)
if err != nil {
t.Skip()
}
t.Logf("str:%q,first:%q,second:%q", str, first, second)
if str != second {
t.Errorf("before: %q, after: %q", str, second)
}
if utf8.ValidString(str) && !utf8.ValidString(first) {
t.Errorf("Reverse produced invalid UTF-8 string %q %q", str, first)
}
})
}Lorsque la fonction d'inversion retourne error, le test est ignoré. Exécutons à nouveau le test flou :
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/219 completed
fuzz: elapsed: 0s, gathering baseline coverage: 219/219 completed, now fuzzing with 16 workers
fuzz: elapsed: 3s, execs: 895571 (297796/sec), new interesting: 32 (total: 251)
fuzz: elapsed: 6s, execs: 1985543 (363120/sec), new interesting: 37 (total: 256)
fuzz: elapsed: 9s, execs: 3087837 (367225/sec), new interesting: 38 (total: 257)
fuzz: elapsed: 12s, execs: 4090817 (335167/sec), new interesting: 40 (total: 259)
fuzz: elapsed: 15s, execs: 5132580 (346408/sec), new interesting: 44 (total: 263)
fuzz: elapsed: 18s, execs: 6248486 (372185/sec), new interesting: 45 (total: 264)
fuzz: elapsed: 21s, execs: 7366827 (373305/sec), new interesting: 46 (total: 265)
fuzz: elapsed: 24s, execs: 8439803 (358059/sec), new interesting: 47 (total: 266)
fuzz: elapsed: 27s, execs: 9527671 (361408/sec), new interesting: 47 (total: 266)
fuzz: elapsed: 30s, execs: 10569473 (348056/sec), new interesting: 48 (total: 267)
fuzz: elapsed: 30s, execs: 10569473 (0/sec), new interesting: 48 (total: 267)
--- PASS: FuzzReverse (30.16s)
=== NAME
PASS
ok golearn/tool_test 30.789sCette fois, nous pouvons obtenir un journal de sortie de test flou plus complet. Voici l'explication de certains concepts :
- elapsed : Temps écoulé après la fin d'un cycle
- execs : Nombre total d'entrées exécutées, 297796/sec indique le nombre d'entrées par seconde
- new interesting : Nombre total d'entrées "intéressantes" ajoutées au corpus pendant le test. (Une entrée intéressante est une entrée qui peut étendre la couverture du code au-delà de la portée couverte par le corpus existant. À mesure que la couverture s'étend continuellement, sa tendance de croissance globale continuera à ralentir)
TIP
S'il n'y a pas de paramètre -fuzztime pour limiter le temps, le test flou s'exécutera indéfiniment.
Types pris en charge
Les types pris en charge dans Go Fuzz sont les suivants :
string,[]byteint,int8,int16,int32/rune,int64uint,uint8/byte,uint16,uint32,uint64float32,float64bool
