Files
fn_registry/docs/testing.md
egutierrez 3a3a8fd9a9 docs: convenciones de testing y schema unit_tests/e2e_tests
Nuevo docs/testing.md con convenciones de test por lenguaje (Go, Python, Bash con 3 opciones), tablas unit_tests y e2e_tests, consultas FTS5 de ejemplo. Actualiza functions.md y CLAUDE.md con referencia a unit_tests.
2026-04-05 18:19:26 +02:00

248 lines
7.4 KiB
Markdown

# Testing
El registry tiene dos niveles de tests:
- **Unit tests** (`unit_tests` en `registry.db`) — tests individuales extraidos automaticamente de los archivos de test de cada funcion.
- **E2E tests** (`e2e_tests` en `operations.db`) — tests de integracion que verifican como las funciones se componen dentro de una app.
---
## Unit tests
`fn index` lee cada archivo de test referenciado por `test_file_path` en las funciones testeadas, extrae los test cases individuales con su codigo, y los inserta en la tabla `unit_tests`.
### Tabla `unit_tests`
| Campo | Tipo | Descripcion |
|---|---|---|
| `id` | string | `{function_id}_t{n}` (ej: `filter_slice_go_core_t0`) |
| `function_id` | string | FK a `functions.id` |
| `name` | string | Nombre del test case |
| `code` | string | Codigo fuente completo del test |
| `file_path` | string | Ruta relativa al archivo de test |
| `lang` | string | Lenguaje (go, py, bash) |
| `created_at` | datetime | Fecha de indexacion |
| `updated_at` | datetime | Fecha de ultima indexacion |
FTS5 disponible sobre `id`, `name`, `code`, `function_id`, `lang`.
### Consultas utiles
```bash
# Todos los tests de una funcion
sqlite3 registry.db "SELECT id, name FROM unit_tests WHERE function_id = 'filter_slice_go_core';"
# Buscar tests por contenido (FTS5)
sqlite3 registry.db "SELECT id, function_id, name FROM unit_tests WHERE id IN (SELECT id FROM unit_tests_fts WHERE unit_tests_fts MATCH 'retry') LIMIT 10;"
# Tests por lenguaje
sqlite3 registry.db "SELECT lang, COUNT(*) FROM unit_tests GROUP BY lang;"
# Ver codigo de un test
sqlite3 registry.db "SELECT code FROM unit_tests WHERE id = 'cache_decorator_py_core_t0';"
```
---
## Convenciones de test por lenguaje
El parser automatico de `fn index` detecta test cases segun el lenguaje. Para que los tests se extraigan correctamente, seguir estas convenciones.
### Go
Convencion estandar de Go. El parser detecta funciones `func TestXxx(t *testing.T)`:
```go
func TestFilterSlice(t *testing.T) {
t.Run("filtra pares", func(t *testing.T) {
got := FilterSlice([]int{1, 2, 3, 4, 5}, func(n int) bool { return n%2 == 0 })
if len(got) != 2 || got[0] != 2 || got[1] != 4 {
t.Errorf("got %v, want [2 4]", got)
}
})
t.Run("slice vacio retorna vacio", func(t *testing.T) {
got := FilterSlice([]int{}, func(n int) bool { return true })
if len(got) != 0 {
t.Errorf("got %v, want []", got)
}
})
}
```
**Deteccion:** `^func (Test\w+)\s*\(` — cada `func Test...` es un test case. Los subtests (`t.Run`) se incluyen dentro del codigo del test padre.
**Archivo:** `{domain}/{name}_test.go` (convencion Go estandar).
### Python
Convencion estandar de pytest. El parser detecta funciones `def test_xxx(`:
```python
def test_funcion_llamada_una_vez(store):
calls = []
@cache_decorator(store, ttl=60)
def compute(x: int) -> int:
calls.append(x)
return x * 10
assert compute(5) == 50
assert compute(5) == 50
assert len(calls) == 1
def test_ttl_expirado(store):
# ...
```
**Deteccion:** `^def (test_\w+)\s*\(` — cada funcion top-level `def test_...` es un test case. El codigo incluye todo hasta la siguiente `def test_` o fin de archivo.
**Archivo:** `{domain}/{name}_test.py`.
### Bash
Bash no tiene framework estandar de testing. El parser soporta tres convenciones, en orden de prioridad:
#### Opcion 1: funciones `test_xxx()` (preferida)
La mas explicita y la que mejor se parsea:
```bash
#!/usr/bin/env bash
source "$(dirname "$0")/mi_funcion.sh"
PASS=0; FAIL=0
assert_eq() {
local name="$1" got="$2" want="$3"
if [ "$got" = "$want" ]; then echo " PASS: $name"; ((PASS++))
else echo " FAIL: $name (got='$got', want='$want')"; ((FAIL++)); fi
}
test_caso_basico() {
local got
got=$(mi_funcion "input")
assert_eq "caso basico" "$got" "expected"
}
test_caso_vacio() {
local got
got=$(mi_funcion "")
assert_eq "input vacio" "$got" ""
}
# Ejecutar todos los tests
test_caso_basico
test_caso_vacio
echo "Resultados: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1
```
**Deteccion:** `^(test_\w+)\s*\(\)\s*\{` — cada funcion `test_xxx() { ... }` es un test case.
#### Opcion 2: secciones `=== nombre ===`
Para tests que agrupan multiples asserts bajo secciones nombradas:
```bash
#!/usr/bin/env bash
source "$(dirname "$0")/mi_funcion.sh"
echo "=== caso basico ==="
got=$(mi_funcion "input")
assert_eq "retorna expected" "$got" "expected"
echo "=== caso edge ==="
got=$(mi_funcion "")
assert_eq "input vacio" "$got" ""
echo "=== errores ==="
assert_fail "input invalido" mi_funcion "--bad"
```
**Deteccion:** `^(echo\s+["'])?===\s*(\w[\w\s]*\w)\s*===(["'])?\s*$` — cada linea con `=== nombre ===` (con o sin `echo`) abre una seccion. El nombre debe contener al menos dos caracteres alfanumericos (las lineas de separacion puras como `======` se ignoran).
#### Opcion 3: comentarios `# Test:`
Para scripts simples donde cada test se marca con un comentario:
```bash
#!/usr/bin/env bash
source "$(dirname "$0")/mi_funcion.sh"
# Test: caso basico
got=$(mi_funcion "input")
[ "$got" = "expected" ] || { echo "FAIL"; exit 1; }
# Test: input vacio
got=$(mi_funcion "")
[ "$got" = "" ] || { echo "FAIL"; exit 1; }
```
**Deteccion:** `^#\s*[Tt]est:\s*(.+)` — cada comentario `# Test: nombre` abre un bloque.
#### Recomendacion
Usar **opcion 1** (funciones `test_xxx()`) para tests nuevos. Es la mas explicita, cada test esta aislado en su propia funcion, y se parsea sin ambiguedad.
La **opcion 2** (secciones `===`) es aceptable cuando ya existe el patron en el archivo (como `pass_test.sh`).
**Archivo:** `{domain}/{name}_test.sh`.
---
## E2E tests
Los e2e tests viven en `operations.db` de cada app. No se extraen automaticamente — se crean manualmente o por el bucle reactivo cuando se necesita verificar que un flujo end-to-end funciona.
### Tabla `e2e_tests`
| Campo | Tipo | Descripcion |
|---|---|---|
| `id` | string | Identificador unico |
| `name` | string | Nombre descriptivo del test |
| `description` | string | Que verifica este test |
| `relation_id` | string | FK a `relations.id` — que pipeline/relacion prueba |
| `steps` | []string | Funciones involucradas en orden |
| `input_fixture` | JSON | Datos de entrada para el test |
| `expected` | JSON | Resultado esperado |
| `last_status` | string | pass, fail, skip, o vacio |
| `last_run_at` | datetime | Ultima ejecucion |
| `execution_id` | string | Referencia a la ejecucion que lo corrio |
| `duration_ms` | int | Duracion en milisegundos |
| `created_at` | datetime | Fecha de creacion |
| `updated_at` | datetime | Ultima actualizacion |
FTS5 disponible sobre `id`, `name`, `description`, `steps`.
### Diferencia con assertions
| | Assertions | E2E tests |
|---|---|---|
| **Que son** | Reglas declarativas sobre datos | Ejecuciones concretas de flujos |
| **Cuando corren** | En cada ejecucion del bucle reactivo | Bajo demanda o en CI |
| **Sobre que** | Una entity (`precio > 0`) | Un flujo completo (input → pipeline → output) |
| **Resultado** | pass/fail sobre el valor actual | pass/fail comparando output vs expected |
### Ejemplo de uso
```bash
# Crear un e2e test para un pipeline
fn ops e2e add --name "metabase_setup_completo" \
--relation-id "rel_setup_metabase" \
--steps '["docker_pull_image_go_infra","init_metabase_go_pipelines"]' \
--input '{"project":"test"}' \
--expected '{"status":"running"}'
# Listar e2e tests
fn ops e2e list
# Ver resultado
fn ops e2e show <id>
```