5d47bc3fe3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9.1 KiB
9.1 KiB
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.NewServercon rutas ad-hoc, SQLite temporal con schema manual, env vars seteadas a mano cont.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 comofilter_sliceomap_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
TestServeryTestDBenfunctions/core/con.mdentypes/core/ - 1.2
test_http_server— creahttptest.Servercon unServeMuxconstruido a partir del map de rutas, retornaTestServercon URL, Client y Cleanup registrado ent.Cleanup - 1.3
test_db_setup— abre SQLite:memory:(o tempfile si se necesita WAL), ejecuta schema SQL, retornaTestDBcon Cleanup registrado ent.Cleanup - 1.4
test_db_seed— iterarows, construye INSERT dinamico por tabla, ejecuta en transaccion - 1.5
assert_json_equal—json.Marshalambos valores, compara conreflect.DeepEqualsobreany, 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 endst - 2.2
test_env_set— itera el map, llamaos.Setenv, construye restore func que restaura los valores originales (oos.Unsetenvsi no existian) - 2.3
test_capture_logs— redirigelog.SetOutputa unbytes.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 indexy verificar confn showcada 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 ent.Cleanuppara que sea automatico, pero tambien retornan la funcion cleanup para uso explicito condefercuando el orden importa. :memory:por defecto en test DB: evita archivos temporales huerfanos. Si un test necesita WAL o acceso concurrente,test_db_setupacepta un flag para crear un tempfile en su lugar.- Assertions retornan
(bool, info)en vez de llamar at.Fatal: las funciones puras no reciben*testing.T. El caller decide si est.Errorot.Fatal. Esto mantiene la pureza deassert_json_equalyassert_contains_all. test_db_seedcon[]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.Tni introducen un DSL de assertions. Se usan conifyt.Errorfestandar de Go.
Riesgos
- Dependencia de
database/sql+ SQLite driver en core: las funcionestest_db_*necesitan el driver SQLite. Mitigado porque el registry ya usamattn/go-sqlite3globalmente 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_logsconlog.SetOutputglobal: en tests paralelos (t.Parallel) esto puede capturar logs de otros tests. Documentar quetest_capture_logsno es safe para tests paralelos, o usar unlog.Loggerpropio pasado como parametro.