El ecosistema de testing de Go está integrado en el propio toolchain: no hace falta un framework externo para escribir tests de calidad. Con go test, table-driven tests, subtests y benchmarks tienes todo lo necesario para la mayoría de proyectos. La librería testify añade aserciones más expresivas cuando las necesitas.
Table-driven tests con t.Run
El patrón más idiomático en Go: una tabla de casos y un bucle que los ejecuta como subtests independientes:
package calculadora_test
import (
"testing"
"example.com/calculadora"
)
func TestDividir(t *testing.T) {
casos := []struct {
nombre string
a, b float64
esperado float64
debeError bool
}{
{"positivos", 10, 2, 5, false},
{"negativo dividendo", -10, 2, -5, false},
{"division por cero", 10, 0, 0, true},
{"decimales", 7, 2, 3.5, false},
}
for _, tc := range casos {
t.Run(tc.nombre, func(t *testing.T) {
resultado, err := calculadora.Dividir(tc.a, tc.b)
if tc.debeError {
if err == nil {
t.Error("esperaba error, no hubo ninguno")
}
return
}
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
if resultado != tc.esperado {
t.Errorf("Dividir(%v, %v) = %v, esperado %v", tc.a, tc.b, resultado, tc.esperado)
}
})
}
}
t.Parallel: tests concurrentes
Llamar a t.Parallel() al principio de un subtest indica que puede ejecutarse en paralelo con otros tests paralelos. Reduce el tiempo total de ejecución en suites grandes:
func TestAPIExterna(t *testing.T) {
endpoints := []string{"/usuarios", "/productos", "/pedidos"}
for _, ep := range endpoints {
ep := ep // captura la variable (Go < 1.22)
t.Run(ep, func(t *testing.T) {
t.Parallel()
resp, err := http.Get("https://api.test" + ep)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("esperado 200, obtenido %d", resp.StatusCode)
}
})
}
}
t.Helper y t.Cleanup
t.Helper() marca una función como auxiliar, de modo que los fallos señalan el punto de llamada, no el interior del helper. t.Cleanup registra funciones que se ejecutan al terminar el test:
func crearBDTest(t *testing.T) *sql.DB {
t.Helper() // los errores señalarán al test que llama, no aquí
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("abrir BD: %v", err)
}
t.Cleanup(func() {
db.Close() // se ejecuta cuando el test termina
})
// Crear esquema
db.Exec("CREATE TABLE usuarios (id INTEGER PRIMARY KEY, nombre TEXT)")
return db
}
func TestRepositorio(t *testing.T) {
db := crearBDTest(t)
repo := NewRepositorio(db)
_, err := repo.Crear("Ana")
if err != nil {
t.Fatal(err)
}
}
Benchmarks con b.N
Go ejecuta el benchmark tantas veces como necesite para obtener una medida estable. b.N aumenta automáticamente en cada iteración:
func BenchmarkConcatenar(b *testing.B) {
palabras := []string{"hola", "mundo", "en", "go"}
b.Run("strings.Join", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strings.Join(palabras, " ")
}
})
b.Run("strings.Builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
for j, p := range palabras {
if j > 0 {
sb.WriteByte(' ')
}
sb.WriteString(p)
}
_ = sb.String()
}
})
}
// go test -bench=. -benchmem
Testify: aserciones más expresivas
testify no reemplaza el testing de Go pero añade aserciones legibles que reducen el código repetitivo:
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestServicio(t *testing.T) {
svc := NewServicio()
usuario, err := svc.CrearUsuario("[email protected]")
require.NoError(t, err) // para si hay error
assert.NotNil(t, usuario)
assert.Equal(t, "[email protected]", usuario.Email)
assert.Greater(t, usuario.ID, 0)
}
testify/mock: mocks tipados
type RepositorioMock struct {
mock.Mock
}
func (m *RepositorioMock) Guardar(u *Usuario) error {
args := m.Called(u)
return args.Error(0)
}
func TestServicioConMock(t *testing.T) {
repo := new(RepositorioMock)
repo.On("Guardar", mock.AnythingOfType("*main.Usuario")).Return(nil)
svc := NewServicio(repo)
err := svc.Registrar("[email protected]")
assert.NoError(t, err)
repo.AssertExpectations(t)
}
testing/fstest: simular sistemas de ficheros
func TestLeerConfig(t *testing.T) {
fs := fstest.MapFS{
"config.yaml": {Data: []byte("puerto: 8080ndebug: true")},
}
cfg, err := LeerConfig(fs, "config.yaml")
require.NoError(t, err)
assert.Equal(t, 8080, cfg.Puerto)
}
