---
id: "0023"
title: "Testing Utilities"
status: completado
type: feature
domain: []
scope: multi-app
priority: media
depends: []
blocks: []
related: []
created: 2026-05-17
updated: 2026-05-17
tags: []
---
# 0023 — Testing Utilities

## Metadata

| Campo | Valor |
|-------|-------|
| **ID** | 0023 |
| **Estado** | pendiente |
| **Prioridad** | media |
| **Tipo** | feature |

## Dependencias

Ninguna.

---

## Objetivo

Crear funciones reutilizables de testing en Go (dominio core) que eliminen el boilerplate repetitivo en tests de apps y funciones del registry: setup de HTTP servers de prueba, bases de datos temporales, fixtures, assertions estructurales y captura de side effects.

## Contexto

- Cada funcion y app que necesita tests construye su propio setup: `httptest.NewServer` con rutas ad-hoc, SQLite temporal con schema manual, env vars seteadas a mano con `t.Cleanup`.
- No hay helpers compartidos. El mismo patron de "crear DB temporal, insertar datos, ejecutar test, limpiar" se repite en decenas de tests con variaciones minimas.
- Go idiomatico usa `t.Helper()` y funciones que retornan cleanup funcs. Estas funciones siguen ese patron exacto.
- Al ser primitivas de testing viven en dominio `core` — son tan fundamentales como `filter_slice` o `map_slice`.

## Arquitectura

```
functions/core/
  test_http_server.go         -- NEW: crea httptest.Server con rutas
  test_http_server.md         -- NEW
  test_db_setup.go            -- NEW: crea SQLite temporal con schema
  test_db_setup.md            -- NEW
  test_db_seed.go             -- NEW: inserta datos semilla en test DB
  test_db_seed.md             -- NEW
  test_fixture_load.go        -- NEW: carga fixture desde JSON/YAML
  test_fixture_load.md        -- NEW
  assert_json_equal.go        -- NEW: deep compare de JSON con diff
  assert_json_equal.md        -- NEW
  assert_contains_all.go      -- NEW: verifica que slice contenga todos los elementos
  assert_contains_all.md      -- NEW
  test_env_set.go             -- NEW: setea env vars con restore func
  test_env_set.md             -- NEW
  test_capture_logs.go        -- NEW: captura log output durante test
  test_capture_logs.md        -- NEW

types/core/
  test_server.md              -- NEW
  test_db.md                  -- NEW
```

## Diseno

### Tipos

```go
// TestServer wrappea httptest.Server con client preconfigurado
type TestServer struct {
    URL     string
    Client  *http.Client
    Cleanup func()
}

// TestDB wrappea una conexion SQLite temporal
type TestDB struct {
    DB      *sql.DB
    Path    string       // "" si es :memory:
    Cleanup func()
}
```

### Funciones

| Funcion | Purity | Firma (simplificada) |
|---------|--------|---------------------|
| `test_http_server` | impure | `(t *testing.T, routes map[string]http.HandlerFunc) TestServer` |
| `test_db_setup` | impure | `(t *testing.T, schema string) TestDB` |
| `test_db_seed` | impure | `(t *testing.T, db *sql.DB, table string, rows []map[string]any) error` |
| `test_fixture_load` | impure | `(t *testing.T, path string, dst any) error` |
| `assert_json_equal` | pure | `(expected, actual []byte) (bool, string)` |
| `assert_contains_all` | pure | `(haystack []string, needles []string) (bool, []string)` |
| `test_env_set` | impure | `(t *testing.T, vars map[string]string) func()` |
| `test_capture_logs` | impure | `(t *testing.T) (*bytes.Buffer, func() []string)` |

Todas las funciones impuras llaman a `t.Helper()` para que los errores apunten al caller en el stack trace.

---

## Tareas

### Fase 1: Tipos + funciones core

- [ ] **1.1** Crear tipos `TestServer` y `TestDB` en `functions/core/` con `.md` en `types/core/`
- [ ] **1.2** `test_http_server` — crea `httptest.Server` con un `ServeMux` construido a partir del map de rutas, retorna `TestServer` con URL, Client y Cleanup registrado en `t.Cleanup`
- [ ] **1.3** `test_db_setup` — abre SQLite `:memory:` (o tempfile si se necesita WAL), ejecuta schema SQL, retorna `TestDB` con Cleanup registrado en `t.Cleanup`
- [ ] **1.4** `test_db_seed` — itera `rows`, construye INSERT dinamico por tabla, ejecuta en transaccion
- [ ] **1.5** `assert_json_equal` — `json.Marshal` ambos valores, compara con `reflect.DeepEqual` sobre `any`, retorna diff legible si difieren
- [ ] **1.6** `assert_contains_all` — recorre needles y acumula los faltantes, retorna `(true, nil)` o `(false, missing)`

### Fase 2: Helpers de side effects + tests + indexado

- [ ] **2.1** `test_fixture_load` — detecta extension `.json`/`.yaml`/`.yml`, lee archivo, decode en `dst`
- [ ] **2.2** `test_env_set` — itera el map, llama `os.Setenv`, construye restore func que restaura los valores originales (o `os.Unsetenv` si no existian)
- [ ] **2.3** `test_capture_logs` — redirige `log.SetOutput` a un `bytes.Buffer`, retorna buffer + funcion que restaura el output original y devuelve las lineas capturadas
- [ ] **2.4** Tests de cada funcion (los helpers se testean a si mismos — meta pero necesario)
- [ ] **2.5** `fn index` y verificar con `fn show` cada ID

---

## Ejemplo de uso

Test de un handler API que lee de SQLite — compone `test_http_server` + `test_db_setup` + `test_db_seed` + `assert_json_equal`:

```go
func TestListUsersHandler(t *testing.T) {
    // 1. DB temporal con schema y datos
    tdb := core.TestDbSetup(t, `
        CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT);
    `)
    // tdb.Cleanup se ejecuta automaticamente al final del test

    core.TestDbSeed(t, tdb.DB, "users", []map[string]any{
        {"id": 1, "name": "Alice", "email": "alice@example.com"},
        {"id": 2, "name": "Bob",   "email": "bob@example.com"},
    })

    // 2. Handler que usa la DB
    handler := func(w http.ResponseWriter, r *http.Request) {
        rows, _ := tdb.DB.Query("SELECT id, name, email FROM users ORDER BY id")
        defer rows.Close()
        var users []map[string]any
        for rows.Next() {
            var id int; var name, email string
            rows.Scan(&id, &name, &email)
            users = append(users, map[string]any{"id": id, "name": name, "email": email})
        }
        json.NewEncoder(w).Encode(users)
    }

    // 3. Server de prueba con la ruta
    ts := core.TestHttpServer(t, map[string]http.HandlerFunc{
        "GET /api/users": handler,
    })
    // ts.Cleanup se ejecuta automaticamente

    // 4. Request y assertion
    resp, _ := ts.Client.Get(ts.URL + "/api/users")
    body, _ := io.ReadAll(resp.Body)
    resp.Body.Close()

    expected := `[{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}]`
    if ok, diff := core.AssertJsonEqual([]byte(expected), body); !ok {
        t.Errorf("response mismatch:\n%s", diff)
    }
}
```

```go
// Env vars aisladas para un test
func TestConfigFromEnv(t *testing.T) {
    restore := core.TestEnvSet(t, map[string]string{
        "DB_PATH":  "/tmp/test.db",
        "API_PORT": "9999",
    })
    defer restore()

    cfg := LoadConfig()
    if cfg.Port != 9999 {
        t.Errorf("expected port 9999, got %d", cfg.Port)
    }
}
```

```go
// Captura de logs para verificar side effects
func TestLogOutput(t *testing.T) {
    buf, finish := core.TestCaptureLogs(t)
    _ = buf // acceso directo al buffer si se necesita

    DoSomethingThatLogs()

    lines := finish() // restaura log output, retorna lineas
    ok, missing := core.AssertContainsAll(lines, []string{"starting", "completed"})
    if !ok {
        t.Errorf("missing log lines: %v", missing)
    }
}
```

## Decisiones de diseno

- **Dominio `core`:** son primitivas de testing tan genericas como las funciones de slice o map. No dependen de ningun dominio especifico.
- **Cleanup via `t.Cleanup` + return:** todas las funciones registran cleanup en `t.Cleanup` para que sea automatico, pero tambien retornan la funcion cleanup para uso explicito con `defer` cuando el orden importa.
- **`:memory:` por defecto en test DB:** evita archivos temporales huerfanos. Si un test necesita WAL o acceso concurrente, `test_db_setup` acepta un flag para crear un tempfile en su lugar.
- **Assertions retornan `(bool, info)` en vez de llamar a `t.Fatal`:** las funciones puras no reciben `*testing.T`. El caller decide si es `t.Error` o `t.Fatal`. Esto mantiene la pureza de `assert_json_equal` y `assert_contains_all`.
- **`test_db_seed` con `[]map[string]any`:** maximo flexibilidad sin necesidad de definir structs. Las keys del map son columnas, los values se insertan con `?` placeholders. Soporta cualquier tabla sin generacion de codigo.
- **No es un framework de testing:** son funciones sueltas que se componen. No reemplazan `testing.T` ni introducen un DSL de assertions. Se usan con `if` y `t.Errorf` estandar de Go.

## Riesgos

- **Dependencia de `database/sql` + SQLite driver en core:** las funciones `test_db_*` necesitan el driver SQLite. Mitigado porque el registry ya usa `mattn/go-sqlite3` globalmente con `-tags fts5`, asi que no es una dependencia nueva.
- **Scope creep hacia un test framework:** mitigado con la regla de que cada funcion es atomica y no hay estado compartido entre ellas. Si se necesitan mas assertions, se anaden como funciones individuales, no como metodos de un `TestSuite`.
- **`test_capture_logs` con `log.SetOutput` global:** en tests paralelos (`t.Parallel`) esto puede capturar logs de otros tests. Documentar que `test_capture_logs` no es safe para tests paralelos, o usar un `log.Logger` propio pasado como parametro.
