Skip to content

Pruebas

Para los desarrolladores, las buenas pruebas pueden detectar errores en el programa con anticipación, evitando la carga mental causada por bugs debido a un mantenimiento inadecuado. Por lo tanto, es muy necesario escribir buenas pruebas. Go proporciona una herramienta de línea de comando muy simple y práctica para pruebas: go test. Las pruebas pueden verse en la biblioteca estándar y en muchos marcos de código abierto. Esta herramienta es muy conveniente de usar y actualmente admite los siguientes tipos de pruebas:

  • Pruebas de ejemplo
  • Pruebas unitarias
  • Pruebas de referencia (benchmark)
  • Pruebas difusas (fuzzing)

En Go, la mayoría de las API son proporcionadas por la biblioteca estándar testing.

TIP

Ejecuta el comando go help testfunc en la línea de comandos para ver la explicación oficial de Go sobre los cuatro tipos de pruebas mencionados anteriormente.

Convenciones de escritura

Antes de comenzar a escribir pruebas, primero debes prestar atención a algunas convenciones, lo que hará que el aprendizaje posterior sea más conveniente.

  • Paquete de prueba: Los archivos de prueba se colocan mejor en un paquete separado, este paquete generalmente se nombra test.
  • Archivos de prueba: Los archivos de prueba generalmente terminan con _test.go. Por ejemplo, si deseas probar una función específica, nómbrala function_test.go. Si deseas dividir aún más según el tipo de prueba, también puedes usar el tipo de prueba como prefijo del archivo, como benchmark_marshaling_test.go o example_marshaling_test.go.
  • Funciones de prueba: Cada archivo de prueba tendrá varias funciones de prueba para diferentes tipos de pruebas. Para diferentes tipos de pruebas, el estilo de nomenclatura de las funciones de prueba también es diferente. Por ejemplo, las pruebas de ejemplo son ExampleXXXX, las pruebas unitarias son TestXXXX, las pruebas de referencia son BenchmarkXXXX y las pruebas difusas son FuzzXXXX. De esta manera, puedes saber qué tipo de prueba es sin necesidad de comentarios.

TIP

Cuando el nombre del paquete es testdata, ese paquete generalmente se usa para almacenar datos auxiliares para pruebas. Al ejecutar pruebas, Go ignorará los paquetes llamados testdata.

Sigue las convenciones anteriores y desarrolla un buen estilo de prueba, lo que te ahorrará muchos problemas en el mantenimiento futuro.

Ejecutar pruebas

El comando go test se usa principalmente para ejecutar pruebas. A continuación, tomemos código real como ejemplo. Ahora tenemos un archivo por probar /say/hello.go con el siguiente código:

go
package say

import "fmt"

func Hello() {
  fmt.Println("hello")
}

func GoodBye() {
  fmt.Println("bye")
}

Y un archivo de prueba /test/example_test.go con el siguiente código:

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
}

Hay varias formas de ejecutar estas pruebas. Por ejemplo, si deseas ejecutar todos los casos de prueba en el paquete test, puedes ejecutar directamente el siguiente comando en el directorio test:

sh
$ go test ./
PASS
ok      golearn/test    0.422s

./ representa el directorio actual. Go recompilará todos los archivos de prueba en el directorio test y luego ejecutará todos los casos de prueba. De los resultados, puedes ver que todos los casos de prueba pasaron. Los parámetros posteriores también pueden seguir múltiples directorios. Por ejemplo, el siguiente comando muestra que el directorio principal del proyecto no tiene archivos de prueba para ejecutar.

sh
$ go test ./ ../
ok      golearn/test
?       golearn [no test files]

TIP

Cuando los parámetros de ejecución tienen múltiples paquetes, Go no volverá a ejecutar los casos de prueba que ya han pasado exitosamente. Al ejecutar, se agregará (cached) al final de la línea para indicar que el resultado de la salida es del caché anterior. Go almacenará en caché los resultados de las pruebas cuando los parámetros de la prueba estén en el siguiente conjunto; de lo contrario, no lo hará.

-benchtime, -cpu,-list, -parallel, -run, -short, -timeout, -failfast, -v

Si deseas deshabilitar el caché, puedes agregar el parámetro -count=1.

Por supuesto, también puedes especificar un archivo de prueba específico para ejecutar.

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

O puedes especificar un caso de prueba específico de un archivo de prueba específico, por ejemplo:

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

Aunque las tres situaciones anteriores completaron las pruebas, los resultados de la salida son demasiado concisos. En este momento, puedes agregar el parámetro -v para hacer que los resultados de la salida sean más detallados, por ejemplo:

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

Ahora puedes ver claramente el orden de ejecución, el tiempo consumido, el estado de ejecución de cada caso de prueba y el tiempo total consumido.

TIP

El comando go test ejecuta por defecto todas las pruebas unitarias, pruebas de ejemplo y pruebas difusas. Si agregas el parámetro -bench, ejecutará todos los tipos de pruebas, como el siguiente comando:

sh
$ go test -bench .

Por lo tanto, necesitas usar el parámetro -run para especificar. Por ejemplo, el comando para ejecutar solo todas las pruebas de referencia es el siguiente:

sh
$ go test -bench . -run ^$

Parámetros comunes

Las pruebas en Go tienen muchos parámetros de bandera. A continuación, solo se presentarán los parámetros comunes. Si deseas obtener más detalles, se recomienda usar el comando go help testflag para consultar por tu cuenta.

ParámetroDescripción
-o fileEspecifica el nombre del archivo binario después de la compilación
-cSolo compila archivos de prueba, pero no los ejecuta
-jsonMuestra registros de prueba en formato json
-exec xprogEjecuta pruebas usando xprog, equivalente a go run
-bench regexpSelecciona pruebas de referencia que coincidan con regexp
-fuzz regexpSelecciona pruebas difusas que coincidan con regexp
-fuzztime tTiempo para que la prueba difusa termine automáticamente, t es el intervalo de tiempo. Cuando la unidad es x, representa el número de veces, por ejemplo 200x
-fuzzminimizetime tTiempo mínimo de ejecución para la prueba de minimización, misma regla que arriba
-count nEjecuta pruebas n veces, por defecto 1 vez
-coverHabilita el análisis de cobertura de pruebas
-covermode set,count,atomicEstablece el modo de análisis de cobertura
-cpuEjecuta GOMAXPROCS para pruebas
-failfastDespués de la primera falla de prueba, no comenzará nuevas pruebas
-list regexpLista casos de prueba que coincidan con regexp
-parallel nPermite que los casos de prueba que llaman a t.Parallel se ejecuten en paralelo, n es el número máximo de paralelismo
-run regexpSolo ejecuta casos de prueba que coincidan con regexp
-skip regexpOmite casos de prueba que coincidan con regexp
-timeout dSi el tiempo de ejecución de una sola prueba excede el intervalo de tiempo d, ocurrirá panic. d es un intervalo de tiempo, por ejemplo 1s, 1ms, 1ns, etc.
-shuffle off,on,NMezcla el orden de ejecución de las pruebas, N es la semilla aleatoria. La semilla predeterminada es la hora del sistema
-vMuestra registros de prueba más detallados
-benchmemEstadísticas de asignación de memoria para pruebas de referencia
-blockprofile block.outEstadísticas de bloqueo de goroutines durante las pruebas y escribe en un archivo
-blockprofilerate nControla la frecuencia de estadísticas de bloqueo de goroutines. Consulta más detalles con el comando go doc runtime.SetBlockProfileRate
-coverprofile cover.outEstadísticas de cobertura de pruebas y escribe en un archivo
-cpuprofile cpu.outEstadísticas de CPU y escribe en un archivo
-memprofile mem.outEstadísticas de asignación de memoria y escribe en un archivo
-memprofilerate nControla la frecuencia de estadísticas de asignación de memoria. Consulta más detalles con el comando go doc runtime.MemProfileRate
-mutexprofile mutex.outEstadísticas de contención de locks y escribe en un archivo
-mutexprofilefraction nEstablece estadísticas de n goroutines compitiendo por un lock mutex
-trace trace.outEscribe el seguimiento de ejecución en un archivo
-outputdir directoryEspecifica el directorio de salida para los archivos de estadísticas mencionados anteriormente. Por defecto es el directorio de ejecución de go test

Pruebas de ejemplo

Las pruebas de ejemplo no son como los otros tres tipos de pruebas para descubrir problemas en el programa. Más bien, se utilizan para mostrar cómo usar una función específica y sirven como documentación. Las pruebas de ejemplo no son un concepto definido oficialmente ni una norma rígida, sino más bien una convención de ingeniería. Si se siguen o no depende del desarrollador. Las pruebas de ejemplo aparecen con mucha frecuencia en la biblioteca estándar, generalmente son ejemplos de código de la biblioteca estándar escritos por el equipo oficial. Por ejemplo, la función de prueba ExampleWithDeadline en context/example_test.go de la biblioteca estándar muestra el uso básico de 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
}

A primera vista, esta función de prueba parece una función ordinaria, pero las pruebas de ejemplo se reflejan principalmente en el comentario Output. Cuando la función por probar tiene solo una línea de salida, se usa el comentario Output para verificar la salida. Primero crea un archivo llamado hello.go y escribe el siguiente código:

go
package say

import "fmt"

func Hello() {
  fmt.Println("hello")
}

func GoodBye() {
  fmt.Println("bye")
}

La función SayHello es la función por probar. Luego crea un archivo de prueba example_test.go y escribe el siguiente código:

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
}

El comentario Output en la función indica que se debe verificar si la salida de la función es hello. A continuación, ejecuta el comando de prueba para ver los resultados.

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

De los resultados, puedes ver que todas las pruebas han pasado. Hay varias formas de escribir Output:

La primera es con una sola línea de salida, lo que significa verificar si la salida de la función es hello:

// Output:
// hello

La segunda es con múltiples líneas de salida, es decir, verificar si la salida coincide en orden:

// Output:
// hello
// bye

La tercera es con salida desordenada, es decir, verificar múltiples líneas de salida sin seguir un orden:

// Unordered output:
// bye
// hello

Cabe destacar que, para las funciones de prueba, solo cuando las últimas líneas son comentarios Output se considerarán pruebas de ejemplo. De lo contrario, será solo una función ordinaria y no será ejecutada por Go.

Pruebas unitarias

Las pruebas unitarias son pruebas de la unidad más pequeña que se puede probar en el software. El tamaño de la unidad depende del desarrollador; puede ser una estructura, un paquete, una función o un tipo. A continuación, lo demostraremos nuevamente con un ejemplo. Primero crea el archivo /tool/math.go y escribe el siguiente código:

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
}

Luego crea el archivo de prueba /tool_test/unit_test.go. Para las pruebas unitarias, el nombre puede ser unit_test o usar el paquete o función que deseas probar como prefijo del archivo.

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)
  }
}

Para las pruebas unitarias, el estilo de nomenclatura de cada caso de prueba es TestXXXX, y el parámetro de entrada de la función debe ser t *testing.T. testing.T es una estructura proporcionada por el paquete testing para facilitar las pruebas y proporciona muchos métodos disponibles. En el ejemplo, t.Errorf es equivalente a t.Logf y se usa para mostrar información de registro formateada cuando la prueba falla. Otros comúnmente usados incluyen t.Fail para marcar el caso actual como fallido. Funciones similares incluyen t.FailNow, que también marcará como fallido, pero el primero continuará ejecutándose después de fallar, mientras que el segundo se detendrá directamente. A continuación se muestra un ejemplo donde se modifica el resultado esperado a un resultado incorrecto:

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 usa internamente t.Fail()
    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 usa internamente t.FailNow()
    t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
  t.Log("test finished")
}

Ejecuta las pruebas anteriores y la salida es la siguiente:

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

De los registros de prueba, puedes ver que el caso TestSum todavía mostró "test finished" aunque falló, mientras que TestEqual no. También está t.SkipNow, que marcará el caso actual como SKIP y luego se detendrá. Se continuará ejecutando en la siguiente ronda de pruebas.

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")
}

Al ejecutar las pruebas, modifica el número de veces de prueba a 2:

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

En el ejemplo anterior, se muestra "test finished" en la última línea para indicar que la prueba ha finalizado. En realidad, puedes usar t.Cleanup para registrar una función de limpieza específicamente para esto. Esta función se ejecutará cuando el caso de prueba termine, como sigue:

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)
  }
}

Después de ejecutar las pruebas, la salida es la siguiente:

$ 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

A través de t.Helper(), puedes marcar la función actual como una función de ayuda. Las funciones de ayuda no se ejecutarán como casos de prueba individuales. Al registrar, el número de línea mostrado será el número de línea del llamador de la función de ayuda. Esto hace que el análisis de registros sea más preciso y evita información redundante. Por ejemplo, el ejemplo de t.Cleanup anterior se puede modificar a una función de ayuda, como sigue:

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)
   }
}

Después de ejecutar las pruebas, la información de salida es la siguiente. La diferencia con antes es que el número de línea de "test finished" se convierte en el número de línea del llamador:

$ 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

Las operaciones anteriores solo se pueden realizar en la prueba principal, es decir, en los casos de prueba ejecutados directamente. Si se usan en subpruebas, causarán panic.

Subpruebas

En algunos casos, necesitarás probar otros casos de prueba dentro de un caso de prueba. Estos casos de prueba anidados generalmente se denominan subpruebas. A través del método t.Run(), cuya firma es la siguiente:

go
// El método Run iniciará una nueva goroutine para ejecutar la subprueba, se bloqueará y esperará hasta que la función f termine antes de devolver
// El valor de retorno indica si la prueba pasó
func (t *T) Run(name string, f func(t *T)) bool

A continuación se muestra un ejemplo:

go
func TestTool(t *testing.T) {
  t.Run("tool.Sum(10,101)", TestSum)
  t.Run("tool.Equal(10,101)", TestEqual)
}

El resultado de la ejecución es el siguiente:

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

A través de la salida, puedes ver claramente la estructura jerárquica padre-hijo. En el ejemplo anterior, la segunda subprueba no se ejecutará hasta que la primera subprueba haya terminado. Puedes usar t.Parallel() para marcar los casos de prueba como ejecutables en paralelo, de modo que el orden de salida será indeterminado.

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")
}

Después de ejecutar las pruebas, la salida es la siguiente:

$ 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

De los resultados de la prueba, puedes ver claramente que hay un proceso de bloqueo y espera. Al ejecutar casos de prueba concurrentemente, el ejemplo anterior definitivamente no se puede realizar normalmente porque el código posterior no puede garantizar la ejecución sincronizada. En este momento, puedes optar por anidar otra capa de t.Run(), como sigue:

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")
}

Ejecuta nuevamente y verás los resultados de ejecución normales:

$ 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

Estilo de tabla

En las pruebas unitarias anteriores, los datos de entrada de prueba se declaran manualmente como variables individuales. Cuando la cantidad de datos es pequeña, no hay problema. Pero si deseas probar múltiples conjuntos de datos, no es práctico declarar variables para crear datos de prueba. Por lo tanto, en general, se recomienda usar la forma de slice de estructuras. Las estructuras son estructuras anónimas declaradas temporalmente. Debido a que este estilo de codificación parece una tabla, se denomina table-driven. A continuación, un ejemplo. Este es un ejemplo donde se declaran manualmente múltiples variables para crear datos de prueba. Si hay múltiples conjuntos de datos, no se ve muy intuitivo. Por lo tanto, se modifica al estilo de tabla:

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)
  }
}

El código modificado es el siguiente:

go
func TestEqual(t *testing.T) {
  t.Cleanup(func() {
    CleanupHelper(t)
  })

  // table driven style
  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)
    }
  }
}

Estos datos de prueba se ven mucho más intuitivos.

Pruebas de referencia

Las pruebas de referencia, también conocidas como pruebas de rendimiento, se utilizan generalmente para probar indicadores de rendimiento como el uso de memoria, el uso de CPU, el tiempo de ejecución, etc. Para las pruebas de referencia, los archivos de prueba generalmente terminan con bench_test.go, y la función del caso de prueba debe tener el formato BenchmarkXXXX.

A continuación, tomemos un ejemplo de comparación de rendimiento de concatenación de cadenas como ejemplo de prueba de referencia. Primero crea el archivo /tool/strConcat.go. Como todos sabemos, usar directamente + para concatenar cadenas tiene un rendimiento muy bajo, mientras que usar strings.Builder es mucho mejor. En el archivo /tool/strings.go, crea dos funciones para realizar concatenación de cadenas de dos maneras:

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)
   }
}

Luego crea el archivo de prueba /tool_test/bench_tool_test.go con el siguiente código:

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)
  }
}

Ejecuta el comando de prueba. El comando habilita registros detallados y análisis de memoria, especifica la lista de núcleos de CPU a usar, y cada caso de prueba se ejecuta dos veces. La salida es la siguiente:

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

A continuación, se explica el resultado de la salida de la prueba de referencia. goos representa el sistema operativo en ejecución, goarch representa la arquitectura de CPU, pkg es el paquete donde se encuentra la prueba, y cpu es información sobre la CPU. El resultado de cada caso de prueba está separado por el nombre de cada prueba de referencia. El 2 en la primera columna BenchmarkConcatDirect-2 representa el número de núcleos de CPU usados. El 4 en la segunda columna representa el tamaño de b.N en el código, que es el número de iteraciones en la prueba de referencia. La tercera columna 277771375 ns/op representa el tiempo consumido por cada iteración, donde ns es nanosegundos. La cuarta columna 4040056736 B/op representa el tamaño en bytes de memoria asignada por cada iteración. La quinta columna 10000 allocs/op representa el número de asignaciones de memoria por cada iteración.

Obviamente, según los resultados de la prueba, el rendimiento de usar strings.Builder es mucho mayor que usar + para concatenar cadenas. Comparar el rendimiento a través de datos intuitivos es el propósito de las pruebas de referencia.

benchstat

benchstat es una herramienta de análisis de pruebas de rendimiento de código abierto. En las pruebas de rendimiento anteriores, solo hay dos muestras. Una vez que hay muchas muestras, el análisis manual será muy laborioso y consume mucho tiempo. Esta herramienta nació para resolver problemas de análisis de rendimiento.

Primero, necesitas descargar la herramienta:

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

Ejecuta las pruebas de referencia dos veces. Esta vez, modifica el número de muestras a 5 y muestra los resultados en los archivos old.txt y new.txt para comparar. El resultado de la primera ejecución:

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

El resultado de la segunda ejecución:

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

Luego usa benchstat para comparar:

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

De los resultados, puedes ver que benchstat los divide en tres grupos: tiempo consumido, uso de memoria y número de asignaciones de memoria. geomean es el promedio, p es el nivel de significancia de la muestra. El intervalo crítico suele ser 0.05. Si es mayor que 0.05, no es muy confiable. Toma uno de los datos como ejemplo:

          │    sec/op     │    sec/op      vs base               │
ConcatDirect-4     894.7m ± ∞ ¹   1123.2m ± ∞ ¹  +25.53% (p=0.008 n=5)

Puedes ver que el tiempo de ejecución de old es 894.7ms, el tiempo de ejecución de new es 1123.2ms, en comparación, el tiempo aumentó un 25.53%.

Pruebas difusas

Las pruebas difusas son una nueva función lanzada en Go 1.18. Es una mejora de las pruebas unitarias y de referencia. La diferencia es que los datos de prueba de las dos anteriores deben ser escritos manualmente por el desarrollador, mientras que las pruebas difusas pueden generar datos de prueba aleatorios a través de un corpus. Para obtener más información sobre las pruebas difusas en Go, puedes visitar Go Fuzzing. La ventaja de las pruebas difusas es que, en comparación con los datos de prueba fijos, los datos aleatorios pueden probar mejor las condiciones límite del programa. A continuación, tomemos el ejemplo del tutorial oficial para explicar. Esta vez, necesitamos probar una función que invierte cadenas. Primero crea el archivo /tool/strings.go y escribe el siguiente código:

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)
}

Crea el archivo de prueba difusa /tool_test/fuzz_tool_test.go y escribe el siguiente código:

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)
    }
  })
}

En las pruebas difusas, primero necesitas agregar datos al corpus de semillas. En el ejemplo, se usa f.Add() para agregar, lo que ayuda a generar datos de prueba aleatorios posteriormente. Luego usa f.Fuzz(fn) para realizar la prueba. La firma de la función es la siguiente:

go
func (f *F) Fuzz(ff any)

func (f *F) Add(args ...any)

fn es similar a la lógica de una función de prueba unitaria. El primer parámetro de entrada de la función debe ser t *testing.T, seguido de los parámetros que deseas generar. Dado que las cadenas pasadas son impredecibles, aquí usamos el método de invertir dos veces para verificar. Ejecuta el siguiente comando:

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

Cuando el parámetro no lleva -fuzz, no se generarán datos de prueba aleatorios. Solo se pasarán los datos del corpus a la función de prueba. De los resultados, puedes ver que todas las pruebas pasaron. Usar de esta manera es equivalente a las pruebas unitarias, pero en realidad hay un problema. A continuación, agrega el parámetro -fuzz y ejecuta nuevamente:

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

Los casos fallidos en las pruebas difusas se mostrarán en un archivo de corpus en el directorio testdata de la carpeta de prueba actual. Por ejemplo, en el ejemplo anterior:

Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
To re-run:
go test -run=FuzzReverse/d856c981b6266ba2

testdata\fuzz\FuzzReverse\d856c981b6266ba2 es la ruta del archivo de corpus de salida. El contenido del archivo es el siguiente:

go test fuzz v1
string("𐑄")

Puedes ver que esta vez no pasó. La razón es que la cadena invertida se convirtió en un formato no utf8. Por lo tanto, a través de las pruebas difusas, se descubrió este problema. Dado que algunos caracteres ocupan más de un byte, si se invierten por byte, definitivamente serán caracteres ilegibles. Por lo tanto, modifica el código fuente por probar de la siguiente manera, convirtiendo la cadena a []rune, para evitar el problema anterior:

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)
}

A continuación, ejecuta directamente el caso fallido de la prueba difusa anterior:

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

Puedes ver que esta vez pasó la prueba. Ejecuta la prueba difusa nuevamente para ver si hay más problemas:

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

Puedes ver que hubo otro error. Esta vez, el problema es que después de invertir la cadena dos veces, no son iguales. El carácter original es \xe4, el resultado esperado es 4ex\, pero el resultado es caracteres ilegibles, como sigue:

go
func main() {
  fmt.Println("\xe4")
  fmt.Println([]byte("\xe4"))
  fmt.Println([]rune("\xe4"))
  fmt.Printf("%q\n", "\xe4")
  fmt.Printf("%x\n", "\xe4")
}

El resultado de la ejecución es:


[65533]
"\xe4"
e4

La razón es que \xe4 representa un byte, pero no es una secuencia UTF-8 válida (en la codificación UTF-8, \xe4 es el comienzo de un carácter de tres bytes, pero faltan los dos bytes siguientes). Al convertir a []rune, Go lo convierte automáticamente en un solo carácter Unicode []rune{"\uFFFD"}. Después de invertirlo, sigue siendo []rune{"\uFFFD"}. Al convertir de nuevo a string, este carácter Unicode se reemplaza con su codificación UTF-8 \xef\xbf\xbd. Por lo tanto, una solución es que si la cadena de entrada no es UTF-8 válida, devuelve directamente un error:

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
}

El código de prueba también necesita modificarse ligeramente:

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)
    }
  })
}

Cuando la función de inversión devuelve error, omite la prueba. Luego ejecuta la prueba difusa:

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

Ahora puedes obtener un registro de salida de prueba difusa más completo. A continuación se explican algunos de los conceptos:

  • elapsed: Tiempo transcurrido después de completar una ronda
  • execs: Número total de entradas ejecutadas, 297796/sec significa cuántas entradas por segundo
  • new interesting: En la prueba, el número total de entradas "interesantes" agregadas al corpus. (Una entrada interesante se refiere a una entrada que puede expandir la cobertura del código más allá del rango cubierto por el corpus existente. A medida que el rango de cobertura se expande continuamente, su tendencia de crecimiento generalmente se ralentizará)

TIP

Si no hay un parámetro -fuzztime para limitar el tiempo, la prueba difusa se ejecutará para siempre.

Tipos soportados

Los tipos soportados en Go Fuzz son los siguientes:

  • string, []byte
  • int, int8, int16, int32/rune, int64
  • uint, uint8/byte, uint16, uint32, uint64
  • float32, float64
  • bool

Golang editado por www.golangdev.cn