¿Qué hace Pruebas unitarias embebidas (basadas en especificación)?
Genera un conjunto de pruebas unitarias para un conjunto de archivos .c/.h que el usuario proporciona, utilizando los 7 pasos de las pruebas basadas en especificación de Maurício Aniche (Effective Software Testing, cap. 2). El juicio que convierte esto en una habilidad: particionar bien el dominio de cada función (el paso más valioso), derivar límites, diseñar el conjunto más pequeño que aún atrape errores, automatizarlo en el framework correcto del proyecto e iterar hasta que la cobertura alcance el objetivo.
Lee references/specification-based-testing.md antes de la Fase 3 — es la calibración sobre cómo pensar en particiones y límites, y las prácticas para evitar un conjunto inflado o frágil.
Invariantes de alcance (léanse primero)
- Salida en inglés. Todo lo que esta habilidad produce está en inglés, incluso cuando el usuario escribe en otro idioma: código de prueba, nombres de prueba, comentarios, catálogo, anotaciones
Covers:, mensajes de salto/ignorar, la declaración de ruta y el resumen final. - Solo pruebas unitarias. Nunca generar pruebas de integración, e2e, sistema, HIL o Robot Framework. La granularidad es la función/módulo aislado, con dependencias simuladas (fakes/mocks).
- Solo los archivos dados. El conjunto es para los
.c/.hque el usuario pasó. No escanear el proyecto en busca de otros objetivos ni probar módulos vecinos por tu cuenta. - Nunca modificar los archivos objetivo — ni temporalmente, ni "para probar y revertir". El conjunto compila los archivos
.coriginales, sin copia en el directorio de pruebas. Para alcanzar algo difícil, usa la API pública, simulaciones de las dependencias o un binario de prueba separado. - Autónomo. Generar, ejecutar y reportar sin pedir confirmación para ejecutar. Hay dos excepciones en las que te detienes y preguntas: (a) Ruta C (ni Zephyr ni ESP-IDF) donde no puedes identificar ningún framework de pruebas unitarias (Fase 0); (b) cada 2 iteraciones de cobertura sin alcanzar el objetivo (Fase 8).
- Sin informe escrito por ti. No escribes un informe de resultados. Ejecuta las herramientas y señala los artefactos que generan (HTML de cobertura, salida del ejecutor). El "resultado" es lo que la herramienta emite, no tu prosa.
- Objetivo de cobertura: función ≥ 95%, línea ≥ 95%, rama ≥ 90% (95/95/90) por defecto. Una vez que se alcanza el objetivo, deja de iterar y enumera las líneas/ramas restantes sin cubrir. Ve a 100/100/100 solo si el usuario lo pide. Por debajo del objetivo solo cuando lo que falta es demostrablemente inalcanzable (ruido de log/assert, limitación de testabilidad del módulo), con cada línea/rama restante justificada.
El flujo (7 pasos de Aniche → 9 fases)
Los 7 pasos no son un pipeline rígido — son iterativos (la Fase 3 a menudo revela algo que te devuelve a la Fase 1). Las fases a continuación son la columna vertebral.
| Fase | Paso de Aniche | Qué haces |
|---|---|---|
| 0 | — | Detectar entorno → Ruta A (Zephyr), B (ESP-IDF) o C (otro) |
| 1 | 1 Entender | Requisito, entradas, salidas de cada función dada |
| 2 | 2 Explorar código | Mapear dependencias para aislar con fakes/mocks |
| 3 | 3 Particiones | Catálogo numerado de particiones (el paso más importante) |
| 4 | 4 Límites | Catálogo numerado de límites (punto on/off) |
| 5 | 5 Diseñar casos | Combinar particiones/límites, reducir combinaciones |
| 6 | 6 Automatizar | Escribir el conjunto en el framework de la ruta, con trazabilidad |
| 7 | — | Ejecutar las herramientas: ejecución + cobertura |
| 8 | 7 Aumentar | Iterar sobre brechas (cobertura + elementos del catálogo sin prueba) |
Fase 0 — Detección del entorno
Identifica el entorno del proyecto que contiene los archivos dados, en este orden, y elige la ruta. Robot Framework, HIL, pytest de integración NO son frameworks de pruebas unitarias — nunca cuentan en ninguna ruta.
-
¿Zephyr? → Ruta A. Señales:
west.yml/west.yaml;CMakeLists.txtconfind_package(Zephyr ...);prj.conf;testcase.yaml/sample.yaml;tests/;ztest/ZTEST_SUITE. Herramientas: twister + ztest + FFF + cobertura. Leereferences/zephyr-path.md. -
¿ESP-IDF? → Ruta B. Señales:
CMakeLists.txtconidf_component_register(;sdkconfig/sdkconfig.defaults; uso de$ENV{IDF_PATH}/idf.py; un componentemain/. Herramientas: Unity nativo de ESP-IDF (compilación host, objetivolinux) + gcov/gcovr. Usa Unity directamente — no te detengas a preguntar. Excepción: si el proyecto ya usa consistentemente otro framework de pruebas unitarias (Ceedling, GoogleTest…), sigue ese. Leereferences/espidf-path.md. -
Otro (ni Zephyr ni ESP-IDF) → Ruta C. Inspecciona la estructura de pruebas unitarias existente (Unity, CMock, Ceedling, GoogleTest, personalizado) y sigue fielmente el patrón del proyecto. Si no se encuentra ningún framework de pruebas unitarias, DETENTE y pregunta al usuario cuál usar — nunca asumas un valor predeterminado silencioso en la Ruta C. Lee
references/non-zephyr-path.md.
Indica en el texto de respuesta (no solo en tu razonamiento), antes de continuar, la ruta elegida y las señales concretas que la justifican, con el archivo y lo que encontraste — ej. "Ruta A (Zephyr): zephyr/west.yml, find_package(Zephyr) en tests/.../CMakeLists.txt, suites ZTEST_SUITE en tests/lib/."
Fase 1 — Entender requisito, entradas y salidas
Para cada función que cubrirá el conjunto, extrae de la lectura del .c/.h:
- la regla de negocio: qué hace y qué no debe hacer (incluyendo esquinas);
- las entradas: tipos, rango/dominio, restricciones entre variables;
- las salidas: cómo las entradas se convierten en el resultado (y efectos secundarios observables: salida escrita, error devuelto, movimiento emitido/no emitido).
Parte de esto es explícito; parte son reglas implícitas del dominio (saturación, envoltura, unidades, signo, histéresis) que infieres. Esas reglas implícitas son donde se esconden los errores.
Fase 2 — Explorar el código
Lee todo el .c/.h y construye el mapa de dependencias a aislar: HAL, controladores, kernel/RTOS, E/S, tiempo, llamadas a otros módulos. Cada dependencia se convierte en un fake/mock en la Fase 6. También identifica cómo se alcanza la lógica real (API pública directa, o funciones static a través de un manejador/vtable — llámalas a través de la API real, nunca inventes una firma).
Fase 3 — Identificar las particiones (el paso más importante)
Aplica los 3 lentes de references/specification-based-testing.md a cada función: cada entrada de forma aislada, entradas en combinación y clases de salida. Considera NULL/vacío y las reglas implícitas del dominio. Si la función tiene estado, también particiona por el estado del módulo/contexto.
Produce, por función, un catálogo nombrado y numerado con IDs estables dentro del conjunto:
- Particiones:
<func>/P1, P2, … - Límites (completados en la Fase 4):
<func>/B1, B2, …
Cada elemento obtiene una descripción breve. Este catálogo es el contrato de trazabilidad del conjunto (materializado en la Fase 6) y la fuente de verdad para las brechas (Fase 8). Una Fase 3 bien hecha debería cubrir casi todo en la primera pasada de cobertura.
Fase 4 — Analizar los límites
Para cada límite entre particiones vecinas, agrega al catálogo el punto on (el valor en la condición) y el punto off (el vecino más cercano en la partición opuesta); las igualdades dan un punto on y dos puntos off. Elimina un límite ya cubierto por otro caso — no dupliques. "¿Partición o límite? No importa": lo que cuenta es que el caso exista.
Fase 5 — Diseñar los casos de prueba
Combina las particiones entre categorías de entrada, pero reduce la explosión combinatoria: prueba lo excepcional (NULL/vacío) una vez y no lo combines con todo; usa el dominio para cortar. Usa una única entrada semilla, variando solo el mínimo por caso (facilita la depuración). En caso de duda, la entrada más simple. Para entradas que no participan en el comportamiento, valores razonables/realistas. Si un caso tiene el mismo esqueleto que varios otros, planifícalo como parametrizado. Enumera los casos y, para cada uno, los IDs del catálogo que ejercita.
Fase 6 — Automatizar (con trazabilidad obligatoria)
Escribe el conjunto en el framework de la ruta detectada (sintaxis, aislamiento y detalles de estructura en references/zephyr-path.md, references/espidf-path.md o references/non-zephyr-path.md).
El catálogo de las Fases 3/4 se convierte en comentarios en el conjunto, a dos niveles:
- Encabezado por función — un bloque al inicio de la sección de pruebas de esa función, que enumera el catálogo completo (cada partición/límite con su ID y breve descripción).
- Anotación por prueba — cada prueba referencia qué elemento(s) ejercita, ej.
/* Covers: P3 + B1 */.
Reglas obligatorias (esto es lo que hace valiosa la trazabilidad):
- Sin prueba huérfana: cada prueba referencia ≥ 1 elemento del catálogo.
- Sin elemento huérfano: cada partición/límite es ejercitado por ≥ 1 prueba. Un elemento sin prueba se marca como brecha (alimenta la Fase 8).
- IDs estables e idénticos entre el encabezado y las anotaciones — para que un
grep "P4"enumere el encabezado y cada prueba que toca esa partición. - Una prueba que cubre una combinación enumera todos los IDs (
/* Covers: P2 + P5 */). Covers:dice la verdad: cada ID anotado coincide con lo que el caso realmente ejercita (entrada, estado y afirmación), no solo un elemento que existe en el catálogo. Antes de la Fase 7, relee las anotaciones contra cada cuerpo de prueba y corrige las que no coincidan.- En las Rutas B y C, el mismo esquema, adaptado a la sintaxis del framework (comentario arriba de cada
TEST(...)/TEST_CASE(...)/TEST_F(...)).
Forma canónica (Ruta A, ztest):
/*
* Catálogo — motor_ctrl_transform (Fase 3 · pruebas basadas en especificación)
* Particiones:
* P1 p_axis_values == NULL -> rechazado, salida no escrita
* P2 axis_count válido (1..N) -> transforma
* P3 valor positivo -> pasos positivos
* P4 valor negativo -> signo preservado
* P5 valor cero -> pasos cero
* Límites:
* B1 axis_count == N (máx) -> aceptado
* B2 axis_count == N+1 -> rechazado
*/
/* Covers: P2 + P3 */
ZTEST(motor_ctrl_suite, test_transform_positive_gives_positive_steps) { /* ... */ }
/* Covers: P1 */
ZTEST(motor_ctrl_suite, test_transform_null_input_is_rejected) { /* ... */ }
Nota: en Zephyr reciente, el ejecutor requiere que los nombres de prueba comiencen con
test_(ZTEST(suite, test_...)) — sin ese prefijo, twister no recoge el caso.
Defecto encontrado: el código diverge de la documentación
Cuando el código contradice lo que el encabezado, el comentario de la función o el contrato de la API documentan (ej. devuelve un código de error diferente al documentado, no restaura el estado en caso de fallo), no dejes el conjunto en rojo y no uses ZTEST_EXPECT_FAIL. Escribe dos pruebas:
- Comportamiento documentado, ignorado con una razón — la afirmación de lo que prometen los documentos, saltada con la razón:
ztest_test_skip()(Ruta A) oTEST_IGNORE_MESSAGE("BUG: ...")(Ruta B). Comentario arriba:/* Covers: Pn — BUG: <docs say X; code does Y> */. - Caracterización — fija el comportamiento actual, para que cualquier cambio (incluyendo la corrección) se muestre. Comentario:
/* Covers: Pn — characterización del BUG anterior */.
El conjunto termina en verde, el error se muestra en cada ejecución (como salto/ignorar) y el binario entra en las mediciones. Sintaxis en los archivos de ruta. Enumera los defectos en el resumen final.
Fase 7 — Ejecución + cobertura
Antes de ejecutar, activa el entorno del proyecto. La herramienta de prueba rara vez está en el PATH global — generalmente vive en un virtualenv del repositorio. No concluyas "no se puede ejecutar" antes de buscar y activar el entorno (esto aplica a cada ruta):
- Busca un venv en la raíz del firmware (
.venv/,venv/,env/) y usa su binario (.venv/bin/west,.venv/bin/python) osource .venv/bin/activate. El venv suele ser un enlace simbólico: verifica contest -e .venv/bin/activateols -laL(que siguen el enlace), nunca solo conls -d/find -type d. Di lo que encontraste: la ruta del venv utilizada, o "no hay venv en<dirs checked>". - Zephyr: establece
ZEPHYR_BASE(ej.deps/zephyr, owest topdir/.west/config) y, para el host,ZEPHYR_TOOLCHAIN_VARIANT=host. Siwestno está en elPATHpero el árbol de Zephyr existe, ejecuta twister directamente:python3 $ZEPHYR_BASE/scripts/twister .... - ESP-IDF: asegúrate de que el entorno IDF esté configurado (
. $IDF_PATH/export.shcuando exista) para la compilación host; de lo contrario, compila congccdirectamente (verespidf-path.md).
Solo después de agotar esta búsqueda es honesto reportar que la ejecución no pudo realizarse aquí.
Economía de contexto (las compilaciones de firmware son verbosas y cada turno relee todo):
- Redirige la compilación, twister y gcovr a un archivo (
> build.log 2>&1) y muestra solo el resumen (tail -n 30 build.log,grep -E 'error|FAIL|PASS|lines|branches' build.log). Abre todo el registro solo cuando el resumen no explique el fallo. - Escribe cada archivo de prueba completo de una vez; corrige con ediciones específicas.
- Agrupa lecturas y verificaciones independientes en el mismo turno; no releas archivos que ya leíste.
Ejecuta la herramienta de la ruta y señala los artefactos que genera (no escribas un informe):
- Ruta A (Zephyr):
west twister --coverage -p native_sim -T <suite_path>. Artefactos:twister-out/coverage/index.html,twister-out/coverage.json,twister-out/twister.xml,twister-out/twister.json. - Ruta B (ESP-IDF): compilación host con Unity +
gcov/gcovr(verespidf-path.md); señala el informe de cobertura (coverage.html/.xml) y la salida de prueba. - Ruta C (otro): el propio ejecutor del proyecto + cobertura (ej.
ceedling gcov, ogcov/gcovrsobre la compilación de prueba). Conserva y señala los artefactos emitidos.
Detalles y solución de problemas en el archivo de cada ruta.
Fase 8 — Iteración de cobertura
Compara la cobertura reportada por las herramientas con el objetivo 95/95/90 (o el que el usuario pidió). Por debajo del objetivo, cada línea/rama alcanzable sin una prueba es una brecha; una vez que se alcanza el objetivo, detente y enumera lo que queda sin cubrir. Para cada brecha, agrega casos usando ambas fuentes: la cobertura numérica (líneas/ramas en rojo) y los elementos del catálogo que faltan.
Después de dos iteraciones de cobertura completas sin alcanzar el objetivo, DETENTE y avisa al usuario en lugar de continuar: "Se alcanzó % de línea, % de rama después de dos iteraciones; elementos del catálogo sin probar. ¿Continúo?" (no preguntes en la primera iteración).
Justificación de cobertura faltante
Cuando marques una línea/rama como "inalcanzable" (porque es código muerto, log solo depuración, aserciones, etc.), escribe la justificación como comentario en el lugar correspondiente de la prueba y menciónala en el resumen final. Las líneas inalcanzables cuentan para el límite inferior: el objetivo se declara < 95/95/90 solo cuando las alcanzables que faltan suman lo que falta.
Fase 9 — Resumen final
Después de la última iteración de la Fase 8, imprime:
- Ruta elegida y por qué.
- Resumen de cobertura (de la última ejecución de la herramienta):
lines: X.X%,branches: Y.Y%,functions: Z.Z%. - Estado del objetivo de cobertura: "Alcanzado" / "No alcanzado" (y por qué).
- Brechas con justificación (líneas/ramas marcadas como inalcanzables).
- Defectos encontrados (saltos por comportamiento documentado vs. real).
- Artefactos generados: enlaces a
twister-out/coverage/index.html(Ruta A),coverage.html(Ruta B) o el informe que emitió la herramienta (Ruta C). - Ubicación del conjunto: ruta del archivo de prueba principal.
No escribas un informe en prosa extensa — usa viñetas y números de herramientas. Esto es lo que el usuario ve. No inventes nombres de archivos de informe que no existan. Si no se ejecutó (Fase 7 no pudo), dilo claramente y da la razón.
Archivos de referencia
Leer antes de la Fase 3:
references/specification-based-testing.md
Archivos por ruta:
references/zephyr-path.mdreferences/espidf-path.mdreferences/non-zephyr-path.md