Files

9.3 KiB

id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags
id title status type domain scope priority depends blocks related created updated tags
0023 Testing Utilities completado feature
multi-app media
2026-05-17 2026-05-17

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

// 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_equaljson.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:

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