Testing en Go
Testing en Go
Go incluye un framework de testing completo en su stdlib. No necesitas dependencias externas para escribir tests robustos.
Estructura basica
Los tests viven en archivos _test.go junto al codigo que prueban.
mipackage/
calculator.go
calculator_test.go
// calculator.go
package calculator
func Sumar(a, b int) int {
return a + b
}
func Dividir(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division por cero")
}
return a / b, nil
}
// calculator_test.go
package calculator
import "testing"
func TestSumar(t *testing.T) {
resultado := Sumar(2, 3)
if resultado != 5 {
t.Errorf("Sumar(2, 3) = %d; esperado 5", resultado)
}
}
Comandos de go test
go test # Ejecuta tests del paquete actual
go test ./... # Ejecuta tests de todos los subpaquetes
go test -v # Modo verbose (muestra cada test)
go test -run TestSum # Filtra tests por nombre (regex)
go test -count=1 # Desactiva cache de tests
Metodos de t *testing.T
| Metodo | Descripcion |
|---|---|
t.Error(args...) | Reporta error, continua ejecucion |
t.Errorf(format, args...) | Error con formato, continua |
t.Fatal(args...) | Reporta error, detiene el test |
t.Fatalf(format, args...) | Fatal con formato |
t.Log(args...) | Log visible con -v |
t.Skip(args...) | Salta el test |
t.Helper() | Marca funcion como helper (mejor stack trace) |
func TestDividir(t *testing.T) {
_, err := Dividir(10, 0)
if err == nil {
t.Fatal("esperaba error por division por cero")
}
resultado, err := Dividir(10, 2)
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
if resultado != 5.0 {
t.Errorf("Dividir(10, 2) = %f; esperado 5.0", resultado)
}
}
Table-driven tests
Patron idiomatico de Go para probar multiples escenarios de forma concisa.
package calculator
import "testing"
func TestSumarTabla(t *testing.T) {
tests := []struct {
nombre string
a, b int
esperado int
}{
{"positivos", 2, 3, 5},
{"negativos", -1, -1, -2},
{"con cero", 5, 0, 5},
{"grandes", 1000000, 1, 1000001},
}
for _, tt := range tests {
t.Run(tt.nombre, func(t *testing.T) {
resultado := Sumar(tt.a, tt.b)
if resultado != tt.esperado {
t.Errorf("Sumar(%d, %d) = %d; esperado %d",
tt.a, tt.b, resultado, tt.esperado)
}
})
}
}
go test -v -run TestSumarTabla
# === RUN TestSumarTabla
# === RUN TestSumarTabla/positivos
# === RUN TestSumarTabla/negativos
# === RUN TestSumarTabla/con_cero
# === RUN TestSumarTabla/grandes
# --- PASS: TestSumarTabla
Table-driven con errores
func TestDividirTabla(t *testing.T) {
tests := []struct {
nombre string
a, b float64
esperado float64
expectErr bool
}{
{"normal", 10, 2, 5.0, false},
{"decimal", 7, 3, 2.3333333333333335, false},
{"division por cero", 10, 0, 0, true},
}
for _, tt := range tests {
t.Run(tt.nombre, func(t *testing.T) {
resultado, err := Dividir(tt.a, tt.b)
if tt.expectErr {
if err == nil {
t.Error("esperaba error, no llego")
}
return
}
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
if resultado != tt.esperado {
t.Errorf("got %f; want %f", resultado, tt.esperado)
}
})
}
}
Subtests con t.Run
t.Run() permite agrupar tests logicamente y ejecutarlos por separado.
func TestOperaciones(t *testing.T) {
t.Run("suma", func(t *testing.T) {
if Sumar(1, 1) != 2 {
t.Error("1 + 1 deberia ser 2")
}
})
t.Run("grupo/division", func(t *testing.T) {
res, _ := Dividir(10, 5)
if res != 2.0 {
t.Error("10 / 5 deberia ser 2")
}
})
}
# Ejecutar solo el subtest de suma
go test -run TestOperaciones/suma
Tests paralelos
func TestParalelo(t *testing.T) {
tests := []struct {
nombre string
input int
expect int
}{
{"caso1", 1, 2},
{"caso2", 2, 4},
{"caso3", 3, 6},
}
for _, tt := range tests {
t.Run(tt.nombre, func(t *testing.T) {
t.Parallel() // Marca el subtest como paralelo
resultado := tt.input * 2
if resultado != tt.expect {
t.Errorf("got %d; want %d", resultado, tt.expect)
}
})
}
}
Importante: Con
t.Parallel()los subtests corren concurrentemente. Asegurate de no compartir estado mutable entre ellos.
Funciones helper
func assertEqual(t *testing.T, got, want int) {
t.Helper() // El error reporta la linea del caller, no del helper
if got != want {
t.Errorf("got %d; want %d", got, want)
}
}
func TestConHelper(t *testing.T) {
assertEqual(t, Sumar(2, 2), 4)
assertEqual(t, Sumar(0, 0), 0)
}
TestMain
Permite setup y teardown global para todos los tests de un paquete.
package mipackage
import (
"os"
"testing"
)
func TestMain(m *testing.M) {
// Setup global
setup()
// Ejecuta todos los tests
code := m.Run()
// Teardown global
teardown()
os.Exit(code)
}
func setup() { /* inicializar DB, etc */ }
func teardown() { /* limpiar recursos */ }
Benchmarks
Miden el rendimiento de funciones. El nombre debe empezar con Benchmark.
package calculator
import "testing"
func BenchmarkSumar(b *testing.B) {
for i := 0; i < b.N; i++ {
Sumar(100, 200)
}
}
func BenchmarkDividir(b *testing.B) {
for i := 0; i < b.N; i++ {
Dividir(100, 3)
}
}
go test -bench=.
# BenchmarkSumar-8 1000000000 0.2500 ns/op
# BenchmarkDividir-8 500000000 2.340 ns/op
go test -bench=BenchmarkSumar -benchmem
# BenchmarkSumar-8 1000000000 0.25 ns/op 0 B/op 0 allocs/op
go test -bench=. -benchtime=5s # Ejecutar por 5 segundos
go test -bench=. -count=3 # Repetir 3 veces
| Flag | Descripcion |
|---|---|
-bench=. | Ejecuta todos los benchmarks |
-benchmem | Muestra allocaciones de memoria |
-benchtime=5s | Duracion del benchmark |
-count=3 | Repeticiones para promediar |
Coverage
go test -cover # Muestra porcentaje de cobertura
go test -coverprofile=cover.out # Genera archivo de cobertura
go tool cover -html=cover.out # Visualiza en navegador
go tool cover -func=cover.out # Muestra cobertura por funcion
Ejemplo de salida:
ok mipackage 0.003s coverage: 85.7% of statements
Mocks con interfaces
Go usa interfaces para inyeccion de dependencias, facilitando los mocks sin librerias externas.
// servicio.go
package user
type UsuarioRepo interface {
ObtenerPorID(id int) (*Usuario, error)
}
type Usuario struct {
ID int
Nombre string
}
type ServicioUsuario struct {
repo UsuarioRepo
}
func NuevoServicio(repo UsuarioRepo) *ServicioUsuario {
return &ServicioUsuario{repo: repo}
}
func (s *ServicioUsuario) ObtenerNombre(id int) (string, error) {
u, err := s.repo.ObtenerPorID(id)
if err != nil {
return "", err
}
return u.Nombre, nil
}
// servicio_test.go
package user
import (
"fmt"
"testing"
)
type mockRepo struct {
usuarios map[int]*Usuario
}
func (m *mockRepo) ObtenerPorID(id int) (*Usuario, error) {
u, ok := m.usuarios[id]
if !ok {
return nil, fmt.Errorf("usuario %d no encontrado", id)
}
return u, nil
}
func TestObtenerNombre(t *testing.T) {
mock := &mockRepo{
usuarios: map[int]*Usuario{
1: {ID: 1, Nombre: "Ana"},
},
}
svc := NuevoServicio(mock)
tests := []struct {
nombre string
id int
esperado string
expectErr bool
}{
{"existente", 1, "Ana", false},
{"no existe", 99, "", true},
}
for _, tt := range tests {
t.Run(tt.nombre, func(t *testing.T) {
nombre, err := svc.ObtenerNombre(tt.id)
if tt.expectErr {
if err == nil {
t.Error("esperaba error")
}
return
}
if err != nil {
t.Fatalf("error inesperado: %v", err)
}
if nombre != tt.esperado {
t.Errorf("got %q; want %q", nombre, tt.esperado)
}
})
}
}
Mencion: testify
Para proyectos grandes, testify ofrece assertions mas legibles:
go get github.com/stretchr/testify
import "github.com/stretchr/testify/assert"
func TestConTestify(t *testing.T) {
assert.Equal(t, 5, Sumar(2, 3))
assert.NotNil(t, resultado)
assert.NoError(t, err)
}
La stdlib es suficiente para la mayoria de proyectos.
testifyes opcional y agrega dependencias.