Go integra el framework de tests en la propia herramienta: go test ./... descubre, compila y ejecuta todos los tests del módulo. No hay dependencias externas, no hay configuración, y el compilador verifica los tests igual que el código de producción. El resultado es que los tests en Go son baratos de escribir y mantener.
Estructura de un fichero de test
// fichero: suma_test.go (mismo paquete o paquete_test)
package suma_test
import (
"testing"
"ejemplo.com/miapp/suma"
)
func TestSumar(t *testing.T) {
resultado := suma.Sumar(2, 3)
if resultado != 5 {
t.Errorf("Sumar(2, 3) = %d; quería 5", resultado)
}
}
Las reglas son: el fichero debe terminar en _test.go, la función debe empezar por Test y su parámetro debe ser *testing.T. El compilador solo incluye estos ficheros cuando ejecutas go test.
t.Error vs t.Fatal
func TestDividir(t *testing.T) {
t.Error("marca el test como fallido pero sigue ejecutándose")
t.Errorf("con formato: esperaba %d, obtuve %d", 5, 3)
t.Fatal("marca como fallido y detiene este test inmediatamente")
t.Fatalf("útil cuando el siguiente paso requiere que este haya funcionado")
}
Table-driven tests: el patrón Go
La forma más idiomática de probar varios casos es con una tabla de casos de prueba:
func TestDividir(t *testing.T) {
casos := []struct {
nombre string
a, b float64
esperado float64
hayError bool
}{
{"normal", 10, 2, 5, false},
{"negativo", -6, 3, -2, false},
{"cero divisor", 5, 0, 0, true},
{"ambos cero", 0, 0, 0, true},
}
for _, tc := range casos {
t.Run(tc.nombre, func(t *testing.T) {
resultado, err := Dividir(tc.a, tc.b)
if tc.hayError {
if err == nil {
t.Errorf("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; quería %v", tc.a, tc.b, resultado, tc.esperado)
}
})
}
}
Subtests con t.Run
t.Run crea un subtest con nombre. Esto permite ejecutarlos individualmente y ver el fallo exacto en la salida:
go test -run TestDividir/cero_divisor -v
Ejecutar tests: flags útiles
go test ./... # todos los paquetes go test -v ./... # verbose: muestra el nombre de cada test go test -run TestSumar # solo los tests que coincidan con el patrón go test -count=1 ./... # desactiva la caché (útil con efectos externos) go test -timeout 30s ./... # timeout global
Testing HTTP con httptest
func TestHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/salud", nil)
rec := httptest.NewRecorder()
SaludHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("código %d; quería 200", rec.Code)
}
if !strings.Contains(rec.Body.String(), "ok") {
t.Errorf("cuerpo %q no contiene 'ok'", rec.Body.String())
}
}
t.Helper: mejorar el reporting de fallos
Si extraes lógica de aserción a una función auxiliar, llama a t.Helper() para que el error muestre la línea del test que llamó al helper, no la línea del helper:
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestSumar(t *testing.T) {
assertEqual(t, Sumar(2, 3), 5) // el error apunta aquí, no en assertEqual
}
TestMain: setup y teardown global
func TestMain(m *testing.M) {
// setup antes de todos los tests
db = iniciarBDTest()
codigo := m.Run() // ejecuta todos los tests
// teardown
db.Close()
os.Exit(codigo)
}
