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