Testing en Go: go test, tabla de tests y subtests

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)
}

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP