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.
This commit is contained in:
+247
@@ -0,0 +1,247 @@
|
||||
# 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>
|
||||
```
|
||||
Reference in New Issue
Block a user