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ómbralafunction_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, comobenchmark_marshaling_test.gooexample_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 sonTestXXXX, las pruebas de referencia sonBenchmarkXXXXy las pruebas difusas sonFuzzXXXX. 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:
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:
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:
$ 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.
$ 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, -vSi 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.
$ go test example_test.go
ok command-line-arguments 0.457sO puedes especificar un caso de prueba específico de un archivo de prueba específico, por ejemplo:
$ go test -run ExampleSay
PASS
ok golearn/test 0.038sAunque 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:
$ 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.040sAhora 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:
$ 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:
$ 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ámetro | Descripción |
|---|---|
-o file | Especifica el nombre del archivo binario después de la compilación |
-c | Solo compila archivos de prueba, pero no los ejecuta |
-json | Muestra registros de prueba en formato json |
-exec xprog | Ejecuta pruebas usando xprog, equivalente a go run |
-bench regexp | Selecciona pruebas de referencia que coincidan con regexp |
-fuzz regexp | Selecciona pruebas difusas que coincidan con regexp |
-fuzztime t | Tiempo 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 t | Tiempo mínimo de ejecución para la prueba de minimización, misma regla que arriba |
-count n | Ejecuta pruebas n veces, por defecto 1 vez |
-cover | Habilita el análisis de cobertura de pruebas |
-covermode set,count,atomic | Establece el modo de análisis de cobertura |
-cpu | Ejecuta GOMAXPROCS para pruebas |
-failfast | Después de la primera falla de prueba, no comenzará nuevas pruebas |
-list regexp | Lista casos de prueba que coincidan con regexp |
-parallel n | Permite que los casos de prueba que llaman a t.Parallel se ejecuten en paralelo, n es el número máximo de paralelismo |
-run regexp | Solo ejecuta casos de prueba que coincidan con regexp |
-skip regexp | Omite casos de prueba que coincidan con regexp |
-timeout d | Si 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,N | Mezcla el orden de ejecución de las pruebas, N es la semilla aleatoria. La semilla predeterminada es la hora del sistema |
-v | Muestra registros de prueba más detallados |
-benchmem | Estadísticas de asignación de memoria para pruebas de referencia |
-blockprofile block.out | Estadísticas de bloqueo de goroutines durante las pruebas y escribe en un archivo |
-blockprofilerate n | Controla la frecuencia de estadísticas de bloqueo de goroutines. Consulta más detalles con el comando go doc runtime.SetBlockProfileRate |
-coverprofile cover.out | Estadísticas de cobertura de pruebas y escribe en un archivo |
-cpuprofile cpu.out | Estadísticas de CPU y escribe en un archivo |
-memprofile mem.out | Estadísticas de asignación de memoria y escribe en un archivo |
-memprofilerate n | Controla la frecuencia de estadísticas de asignación de memoria. Consulta más detalles con el comando go doc runtime.MemProfileRate |
-mutexprofile mutex.out | Estadísticas de contención de locks y escribe en un archivo |
-mutexprofilefraction n | Establece estadísticas de n goroutines compitiendo por un lock mutex |
-trace trace.out | Escribe el seguimiento de ejecución en un archivo |
-outputdir directory | Especifica 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:
// 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:
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:
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.
$ 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.448sDe 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:
// helloLa segunda es con múltiples líneas de salida, es decir, verificar si la salida coincide en orden:
// Output:
// hello
// byeLa tercera es con salida desordenada, es decir, verificar múltiples líneas de salida sin seguir un orden:
// Unordered output:
// bye
// helloCabe 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:
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.
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:
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:
$ 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.037sDe 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.
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 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.468sEn 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:
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.462sHelper
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:
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.464sTIP
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:
// 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)) boolA continuación se muestra un ejemplo:
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:
$ 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.449sA 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.
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.444sDe 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:
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.450sEstilo 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:
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:
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:
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:
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:
$ 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.381sA 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:
$ go install golang.org/x/perf/benchstatEjecuta 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:
$ 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.742sEl resultado de la segunda ejecución:
$ 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.387sLuego usa benchstat para comparar:
$ 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 equalDe 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:
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:
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:
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:
$ 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.539sCuando 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:
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/217 completed
fuzz: minimizing 91-byte failing input file
fuzz: elapsed: 0s, gathering baseline coverage: 15/217 completed
--- FAIL: FuzzReverse (0.13s)
--- FAIL: FuzzReverse (0.00s)
fuzz_tool_test.go:18: str:"𐑄",first:"\x84\x91\x90\xf0",second:"𐑄"
fuzz_tool_test.go:23: Reverse produced invalid UTF-8 string "𐑄" "\x84\x91\x90\xf0"
Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
To re-run:
go test -run=FuzzReverse/d856c981b6266ba2
=== NAME
FAIL
exit status 1
FAIL golearn/tool_test 0.697sTIP
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/d856c981b6266ba2testdata\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:
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:
$ 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.033sPuedes ver que esta vez pasó la prueba. Ejecuta la prueba difusa nuevamente para ver si hay más problemas:
$ 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.184sPuedes 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:
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"
e4La 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:
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:
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:
$ 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.789sAhora 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,[]byteint,int8,int16,int32/rune,int64uint,uint8/byte,uint16,uint32,uint64float32,float64bool
