Skip to content

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-le function_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 exemple benchmark_marshaling_test.go ou example_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 sont TestXXXX, les tests de référence sont BenchmarkXXXX, et les tests flous sont FuzzXXXX. 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 :

go
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 :

go
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 :

sh
$ 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.

sh
$ 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, -v

Si 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.

sh
$ go test example_test.go
ok      command-line-arguments  0.457s

Ou vous pouvez spécifier un cas de test particulier dans un fichier de test, par exemple :

sh
$ go test -run ExampleSay
PASS
ok      golearn/test    0.038s

Bien 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 :

sh
$ 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.040s

Maintenant, 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 :

sh
$ 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 :

sh
$ 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ètreDescription
-o fileSpécifie le nom du fichier binaire après compilation
-cCompile uniquement les fichiers de test, sans les exécuter
-jsonAffiche les journaux de test au format JSON
-exec xprogExécute les tests en utilisant xprog, équivalent à go run
-bench regexpSélectionne les tests de référence correspondant à regexp
-fuzz regexpSélectionne les tests flous correspondant à regexp
-fuzztime tTemps d'exécution automatique des tests flous, t est l'intervalle de temps
-fuzzminimizetime tTemps minimum d'exécution des tests de minimisation
-count nExécute les tests n fois, par défaut 1 fois
-coverActive l'analyse de la couverture des tests
-covermode set,count,atomicDéfinit le mode d'analyse de la couverture
-cpuExécute GOMAXPROCS pour les tests
-failfastAprès le premier échec de test, ne démarre pas de nouveaux tests
-list regexpListe les cas de test correspondant à regexp
-parallel nPermet l'exécution parallèle des tests appelant t.Parallel, n est le maximum
-run regexpExécute uniquement les cas de test correspondant à regexp
-skip regexpIgnore les cas de test correspondant à regexp
-timeout dSi un test dépasse le temps d, il y a panic. d est un intervalle de temps
-shuffle off,on,NMélange l'ordre d'exécution des tests, N est la graine aléatoire
-vAffiche des journaux de test plus détaillés
-benchmemStatistiques d'allocation de mémoire pour les tests de référence
-blockprofile block.outStatistiques de blocage des goroutines et écriture dans un fichier
-blockprofilerate nContrôle la fréquence des statistiques de blocage
-coverprofile cover.outStatistiques de couverture des tests et écriture dans un fichier
-cpuprofile cpu.outStatistiques CPU et écriture dans un fichier
-memprofile mem.outStatistiques d'allocation de mémoire et écriture dans un fichier
-memprofilerate nContrôle la fréquence des statistiques d'allocation de mémoire
-mutexprofile mutex.outStatistiques de contention de verrous et écriture dans un fichier
-mutexprofilefraction nDé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 directorySpé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 :

go
// 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 :

go
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 :

go
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.

sh
$ 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.448s

D'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:
// hello

La deuxième est plusieurs lignes de sortie, c'est-à-dire vérifier si la sortie correspond dans l'ordre :

// Output:
// hello
// bye

La troisième est une sortie non ordonnée, c'est-à-dire vérifier si plusieurs lignes de sortie correspondent sans ordre :

// Unordered output:
// bye
// hello

Il 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 :

go
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.

go
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 :

go
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 :

sh
$ 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.037s

D'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.

go
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 :

sh
$ 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.468s

Dans 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 :

go
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 :

sh
$ 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.462s

Helper

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 :

go
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.

sh
$ 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.464s

TIP

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 :

go
// 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)) bool

Voici un exemple :

go
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 :

sh
$ 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.449s

D'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é.

go
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 :

sh
$ 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.444s

D'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 :

go
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.

sh
$ 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.450s

Style 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 :

go
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 :

go
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 :

go
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 :

go
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 :

sh
$ 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.381s

Voici 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 :

sh
$ go install golang.org/x/perf/benchstat

Exé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 :

sh
$ 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.742s

Deuxième résultat d'exécution :

sh
$ 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.387s

Ensuite, utilisez benchstat pour comparer :

sh
$ 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 equal

D'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 :

go
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 :

go
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 :

go
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 :

sh
$ 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.539s

Lorsque 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 :

sh
$ 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.697s

TIP

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/d856c981b6266ba2

testdata\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 :

go
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 :

sh
$ 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.033s

On 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 :

sh
$ 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.184s

On 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 :

go
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"
e4

La 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 :

go
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é :

go
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 :

sh
$ 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.789s

Cette 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, []byte
  • int, int8, int16, int32/rune, int64
  • uint, uint8/byte, uint16, uint32, uint64
  • float32, float64
  • bool

Golang by www.golangdev.cn edit