Intro
Testing package
El ecosistema de Go trae soporte nativo para escribir y ejecutar tests automáticos. El paquete testing es el que provee soporte para la escritura de tests.
Unit test
Para definir una test suite es necesario que el fichero termine en: _test.go. Definir un test case se tiene que escribir una función con la forma:
func TestXxx(t *testing.T)
Donde Xxx es el nombre del caso de prueba y debe comenzar con mayúsculas.
Para correr los tests basta con ejecutar go test y automáticamente buscará las suites y correrá los tests.
go test
Pongamos un ejemplo sencillo para ilustrar los conceptos. Supongamos un paquete que se encarga de realizar operaciones matemáticas: sumas, restas, multiplicaciones y divisiones con su respectivo test.
- math.go
package main
func Add(a, b int) int {
return a + b
}
- math_test.go
package main
import "testing"
func TestAddZeroValues(t *testing.T) {
res := Add(0, 0)
if res != 0 {
t.Errorf("Expected 0, got %d\n", res)
}
}
func TestSubZeroValues(t *testing.T) {
res := Sub(0, 0)
if res != 0 {
t.Errorf("Expected 0, got %d\n", res)
}
}
Por último para ejecutar el test y mostrar la salida de cada test:
go test -v
Table-Driven test
El table-driven test es un concepto para la escritura de tests que consiste en escribir una matriz que contenga las entradas y salidas de los esperadas para un caso de prueba. Esta técnica nos evita tener que escribir múltiples casos de prueba para diferentes entradas.
func TestTableDrivenDiv(t *testing.T) {
var table = []struct {
name string
input_a int
input_b int
expected int
}{
{"Divide 1/1", 1, 1, 1},
{"Divide -1/1", -1, 1, -1},
{"Divide 0/1", 0, 1, 0},
{"Divide 2/1", 2, 1, 2},
}
for _, test := range table {
t.Run(test.name, func(t *testing.T) {
res := Div(test.input_a, test.input_b)
if res != test.expected {
t.Errorf("Expected %d, got %d\n", test.expected, res)
}
})
}
}
Coverage
Se puede conocer el porcentaje de cobertura de nuestro código con go test:
go test -cover
Benchmark
Para escribir benchmarks:
Las funciones con la forma:
func BenchmarkXxx(t *testing.B)
Se consideran benchmarks
Un ejemplo de Benchmark:
func BenchmarkRandInt(b *testing.B) {
for i := 0; i < b.N; i++ {
rand.Int()
}
}
Para correr un benchmark se tiene que correr explícitamente con:
go test -bench=.
Fuzz test
Fuzzy testing es una técnica de testing que consiste en introducir entradas no esperadas al elemento de prueba.
Go también provee de esta funcionalidad de forma nativa. Para definir un Fuzz test:
func FuzzXxx (f *testing.F)
Para ilustrarlo, remotomemos el ejemplo anterior y creemos un Fuzz test para la función Div:
func FuzzDiv(f *testing.F) {
f.Fuzz(func (t *testing.T, a, b int) {
Div(a, b)
})
}
Como se puede ver f.Fuzz acepta múltiples argumentos para usar como entrada a nuestro caso de prueba.
Deliberadamente he dejado que el denominador pueda tener el valor cero y por lo tanto la función Div contenga un error. El Fuzz test va a conseguir reproducir esa condición de error. Para ejecutarlo y ver la salida:
$ go test -fuzz=.
fuzz: elapsed: 0s, gathering baseline coverage: 0/1 completed
failure while testing seed corpus entry: FuzzDiv/7bef6c8ac710a3fe
fuzz: elapsed: 0s, gathering baseline coverage: 0/1 completed
--- FAIL: FuzzDiv (0.01s)
--- FAIL: FuzzDiv (0.00s)
testing.go:1485: panic: runtime error: integer divide by zero
goroutine 20 [running]:
runtime/debug.Stack()
/usr/lib/go/src/runtime/debug/stack.go:24 +0x9e
testing.tRunner.func1()
/usr/lib/go/src/testing/testing.go:1485 +0x1f6
panic({0x5c32e0, 0x721fc0})
/usr/lib/go/src/runtime/panic.go:884 +0x213
test.Div(...)
/tmp/test/math.go:16
test.FuzzDiv.func1(0x0?, 0x0?, 0x0?)
/tmp/test/math_test.go:64 +0x3d
reflect.Value.call({0x5bf720?, 0x5fc668?, 0x13?}, {0x5ed030, 0x4}, {0xc0000a01e0, 0x3, 0x4?})
/usr/lib/go/src/reflect/value.go:586 +0xb0b
reflect.Value.Call({0x5bf720?, 0x5fc668?, 0x6f31a0?}, {0xc0000a01e0?, 0x5ec720?, 0xc00009a058?})
/usr/lib/go/src/reflect/value.go:370 +0xbc
testing.(*F).Fuzz.func1.1(0x0?)
/usr/lib/go/src/testing/fuzz.go:335 +0x3f3
testing.tRunner(0xc0000824e0, 0xc0000b0090)
/usr/lib/go/src/testing/testing.go:1576 +0x10b
created by testing.(*F).Fuzz.func1
/usr/lib/go/src/testing/fuzz.go:322 +0x5b9
FAIL
exit status 1
FAIL test 0.008s
Testify
Testify es un paquete de pruebas unitarias de Go que proporciona funciones adicionales y mejoras a la biblioteca estándar de pruebas de Go. Testify usa una sintaxis similar a otros frameworks de pruebas de otros lenguajes de programación.
Por ejemplo testify provee del paquete assert que provee una sintaxis legible para hacer nuestros tests como:
assert.Equal()
assert.True()
assert.Nil()
Para instalar testify:
go get github.com/stretchr/testify
Usando testify en nuestro proyecto:
package main
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestTableDrivenDiv(t *testing.T) {
var table = []struct {
name string
input_a int
input_b int
expected int
}{
{"Divide 1/1", 1, 1, 1},
{"Divide -1/1", -1, 1, -1},
{"Divide 0/1", 0, 1, 0},
{"Divide 2/1", 2, 1, 2},
}
for _, test := range table {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, Div(test.input_a, test.input_b), test.expected, "")
})
}
}
Links
https://blog.jetbrains.com/go/2022/11/22/comprehensive-guide-to-testing-in-go/#WritingFuzzTests