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

7.4 KiB

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

# 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):

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(:

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:

#!/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:

#!/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:

#!/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

# 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>