---
name: go-testing
description: >
  Testing Go con table-driven tests, testify, httptest y benchmarks. Cubre
  subtests con t.Run, mocks con interfaces, tests concurrentes y profiling
  básico. Cargar cuando se escriban tests en Go o se configure cobertura.
version: "1.0.0"
herramientasPermitidas: [Read, Grep]
exclusiones:
  - "No cargar para implementar el código de producción que se va a testear — primero implementar con `go-experto`, luego escribir tests."
  - "No cargar para diseñar la arquitectura de paquetes Go — para eso cargar `go-patrones`."
  - "No cargar para errores de compilación Go en los tests — si el test no compila, cargar `build-errors-go`."
  - "No cargar para benchmarking de base de datos o SQL — los benchmarks de BD tienen patrones de teardown que requieren el skill específico de BD."
evolvable: true  # default para skill estandar
---
# Go Testing — Table-Driven Tests y Ecosystem de Testing

Cubre las prácticas de testing Go idiomático: table-driven tests, subtests,
testify para assertions, httptest para handlers HTTP, t.Parallel() y benchmarks.

## Cuándo cargar este skill

Invoca `Skill("go-testing")` cuando:

- Se escriban tests unitarios o de integración en Go
- Se testeen HTTP handlers con httptest
- Se configuren benchmarks o profiling con testing.B
- Se implementen mocks mediante interfaces
- Se use t.Parallel() para acelerar la suite

## Cuándo NO cargar

- El código de producción aún no está implementado — primero implementar con `go-experto`, luego escribir los tests.
- La pregunta es sobre diseñar la arquitectura de paquetes Go — para eso cargar `go-patrones`.
- El test no compila por un error de Go — cargar `build-errors-go` para diagnosticar el error de compilación.
- Los tests son de otro lenguaje — este skill cubre el ecosistema de testing de Go (table-driven, testify, httptest).

## Conceptos clave

### Table-Driven Tests

El patrón estándar Go: una slice de structs donde cada elemento es un caso de
prueba con nombre, inputs y resultado esperado. Cada caso se ejecuta con t.Run
como subtest independiente y paralelo.

### Interfaces como mecanismo de mock

Go no tiene reflection-based mocking por defecto. El patrón idiomático es
definir interfaces pequeñas y proporcionar implementaciones falsas en el package
de test.

### httptest para handlers HTTP

`httptest.NewRecorder()` captura la respuesta; `httptest.NewServer()` levanta
un servidor real para tests de integración HTTP.

## Reglas obligatorias

### Nombres de tabla descriptivos — usar campo "name" o "desc"

Cada caso en la tabla tiene un campo string que lo describe. El t.Run usa ese
campo como nombre del subtest, haciendo el output legible.

### t.Parallel() en tests independientes

Los tests sin estado compartido deben llamar `t.Parallel()` al inicio para
habilitar ejecución concurrente y reducir el tiempo total de la suite.

### Usar testify/assert — no comparacion manual con if

`assert.Equal` y `require.Equal` producen mensajes con diff; una comparación
manual con `if got != want { t.Errorf(...) }` omite el contexto.
`require` detiene el test en el primer fallo; `assert` acumula todos los fallos.

### Cerrar el body en tests de integracion HTTP

```go
resp, err := http.Get(ts.URL + "/v1/pedidos")
require.NoError(t, err)
defer resp.Body.Close() // SIEMPRE defer antes de leer
```

## Patrones recomendados

### Table-driven test completo

```go
func TestCalcularDescuento(t *testing.T) {
    t.Parallel()

    casos := []struct {
        name      string
        precio    float64
        categoria string
        esperado  float64
    }{
        {name: "sin descuento para electronico", precio: 100, categoria: "electronico", esperado: 100},
        {name: "10% descuento para ropa",        precio: 200, categoria: "ropa",        esperado: 180},
        {name: "precio cero retorna cero",       precio: 0,   categoria: "ropa",        esperado: 0},
    }

    for _, tc := range casos {
        tc := tc // captura de variable para t.Parallel() en subtests
        t.Run(tc.name, func(t *testing.T) {
            t.Parallel()
            got := calcularDescuento(tc.precio, tc.categoria)
            assert.InDelta(t, tc.esperado, got, 0.001)
        })
    }
}
```

### Mock con interfaz

```go
// Interfaz en el package de producción
type PedidoRepository interface {
    Obtener(ctx context.Context, id uuid.UUID) (*Pedido, error)
    Guardar(ctx context.Context, p *Pedido) error
}

// Implementación falsa en el package de test
type repoFalso struct {
    pedidos map[uuid.UUID]*Pedido
    err     error
}
func (r *repoFalso) Obtener(_ context.Context, id uuid.UUID) (*Pedido, error) {
    if r.err != nil { return nil, r.err }
    return r.pedidos[id], nil
}
func (r *repoFalso) Guardar(_ context.Context, p *Pedido) error {
    r.pedidos[p.ID] = p
    return r.err
}

// Uso en test
func TestPedidoService_Confirmar(t *testing.T) {
    repo := &repoFalso{pedidos: make(map[uuid.UUID]*Pedido)}
    svc := NewPedidoService(repo)
    // ...
}
```

### httptest con servidor real

```go
func TestPedidoHandler_Crear(t *testing.T) {
    t.Parallel()

    handler := NuevoRouter(pedidoHandlerFalso())
    ts := httptest.NewServer(handler)
    defer ts.Close()

    body := strings.NewReader(`{"clienteId":"uuid-123","items":[]}`)
    resp, err := http.Post(ts.URL+"/v1/pedidos", "application/json", body)
    require.NoError(t, err)
    defer resp.Body.Close()

    assert.Equal(t, http.StatusCreated, resp.StatusCode)

    var resultado map[string]interface{}
    require.NoError(t, json.NewDecoder(resp.Body).Decode(&resultado))
    assert.NotEmpty(t, resultado["id"])
}
```

### httptest.NewRecorder para tests unitarios de handler

```go
func TestPedidoHandler_Obtener_NotFound(t *testing.T) {
    repo := &repoFalso{err: ErrNotFound}
    handler := NuevoPedidoHandler(NewPedidoService(repo))

    req := httptest.NewRequest(http.MethodGet, "/v1/pedidos/uuid-404", nil)
    rec := httptest.NewRecorder()

    handler.Obtener(rec, req)

    assert.Equal(t, http.StatusNotFound, rec.Code)
}
```

### Benchmark

```go
func BenchmarkCalcularDescuento(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        calcularDescuento(100.0, "ropa")
    }
}
```

Correr con: `go test -bench=. -benchmem ./...`

### Golden files para outputs complejos

```go
func TestGenerarReporte(t *testing.T) {
    got := generarReporte(pedidosDePrueba())
    golden := filepath.Join("testdata", "reporte_esperado.json")

    if *update { // flag: go test -update
        os.WriteFile(golden, got, 0644)
    }

    esperado, err := os.ReadFile(golden)
    require.NoError(t, err)
    assert.JSONEq(t, string(esperado), string(got))
}
```

## Anti-patrones conocidos

### Test sin nombre en tabla — output ilegible

```go
// MAL — falla como "--- FAIL: TestCalc/#02"
casos := []struct{ input, want float64 }{
    {100, 80},
    {0, 0},
}

// BIEN — falla como "--- FAIL: TestCalc/precio_cero"
casos := []struct {
    name  string
    input float64
    want  float64
}{
    {name: "descuento estandar", input: 100, want: 80},
    {name: "precio cero",        input: 0,   want: 0},
}
```

### Captura de variable de loop sin tc := tc

```go
// MAL — todos los subtests paralelos leen el mismo tc (el ultimo)
for _, tc := range casos {
    t.Run(tc.name, func(t *testing.T) {
        t.Parallel()
        // tc.input es siempre el ultimo caso
    })
}

// BIEN — captura local
for _, tc := range casos {
    tc := tc
    t.Run(tc.name, func(t *testing.T) {
        t.Parallel()
        // tc.input es el caso correcto
    })
}
```

### io.Discard en lugar de leer el body en tests de integración

```go
// MAL — el body queda abierto y la conexión no se libera
resp, _ := http.Get(url)
// sin defer resp.Body.Close()

// BIEN
resp, err := http.Get(url)
require.NoError(t, err)
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body) // drenar si no se necesita el contenido
```

## Checklist de verificación

- [ ] Todos los casos de tabla tienen campo "name" descriptivo
- [ ] tc := tc antes de t.Parallel() en subtests con tabla
- [ ] defer resp.Body.Close() en todo test que hace request HTTP
- [ ] Mocks implementan interfaces pequeñas, no structs completos
- [ ] require para fallos que invalidan el resto del test; assert para los demás
- [ ] Benchmarks con b.ReportAllocs() para detectar allocaciones innecesarias
- [ ] t.Parallel() en tests sin estado compartido

## Gotchas / Errores comunes no obvios

**`tc := tc` antes de `t.Parallel()` en table-driven tests con closures**: sin esta captura, todos los goroutines del subtest comparten la variable de loop y leen el último valor al ejecutarse. Go 1.22+ corrigió el comportamiento de loop variables, pero en proyectos con `go 1.21` o menor en `go.mod` sigue siendo necesario. Fix: verificar la versión de Go en `go.mod`; si es < 1.22, agregar `tc := tc` antes de `t.Parallel()` en todos los table-driven tests.

**`httptest.NewRecorder()` no replica el comportamiento de flujo de `http.ResponseWriter`**: `ResponseRecorder` no llama `WriteHeader(200)` implícitamente hasta que se llama `Write()`, mientras que un cliente real sí recibe el status code. Un handler que usa `w.WriteHeader()` seguido de `w.Write()` funciona diferente si se inspecciona el recorder antes de que el handler termine. Fix: siempre inspeccionar `recorder.Result()` después de que el handler ha terminado, no durante.

**`t.Cleanup()` registrado dentro de un `t.Run()` solo aplica al subtest, no al test padre**: si un helper registra `t.Cleanup(...)` con el `t` del padre pero el recurso se crea en un subtest, el cleanup puede ejecutarse antes de que el subtest termine. Fix: pasar el `t` correcto (del subtest) al helper que registra el cleanup, no el `t` del test padre.

**Benchmarks con `b.StopTimer()`/`b.StartTimer()` que no incluyen el setup inicial miden erróneamente**: si el setup costoso está fuera de la función de benchmark (en `b.ResetTimer()` al inicio), las primeras iteraciones incluyen tiempo de calentamiento. Fix: usar `b.ResetTimer()` inmediatamente después del setup costoso para que las `b.N` iteraciones empiecen desde cero.

---
*Skill creado con swl:crear-skill el 2026-03-31. Versión 1.0.0.*
