CommunityRedacción y edicióngithub.com

Pruebas unitarias embebidas (basadas en especificación)

Genera un conjunto de pruebas unitarias para archivos .c/.h de firmware embebido, siguiendo el enfoque de pruebas basadas en especificación de Maurício Aniche (particiones → límites → casos → cobertura). Detecta el entorno: Zephyr usa twister + ztest + FFF en native_sim; ESP-IDF usa Unity nativo en una compilación host + gcovr; cualquier otro entorno sigue el framework de pruebas existente del proyecto (Unity, CMock, Ceedling, GoogleTest...) o se detiene y pregunta si no hay ninguno. Úsalo SIEMPRE que el usuario pida generar/escribir/crear pruebas unitarias para módulos .c/.h.

¿Qué es Pruebas unitarias embebidas (basadas en especificación)?

Esta habilidad automatiza la generación de un conjunto completo de pruebas unitarias para archivos .c/.h en proyectos de firmware embebido, siguiendo rigurosamente el proceso de pruebas basadas en especificación de Maurício Aniche (Effective Software Testing, capítulo 2). El valor principal radica en la partición precisa del dominio de cada función, la derivación de límites y el diseño de la suite más pequeña que aún atrape errores, automatizándola en el framework adecuado del proyecto. Soporta tres entornos principales: Zephyr (usa twister + ztest + FFF en native_sim), ESP-IDF (usa Unity nativo en compilación host + gcovr) y cualquier otro proyecto (sigue el framework existente: Unity, CMock, Ceedling, GoogleTest). La habilidad se ejecuta en 9 fases, desde la detección del entorno hasta la iteración de cobertura, garantizando un contrato de trazabilidad mediante un catálogo numerado de particiones y límites. Está diseñada para ser autónoma: genera, ejecuta y reporta sin pedir confirmación, excepto en dos casos: cuando no se identifica ningún framework de pruebas unitarias (ruta C) o después de dos iteraciones de cobertura sin alcanzar el objetivo. El objetivo de cobertura predeterminado es 95% de funciones, 95% de líneas y 90% de ramas (95/95/90). La salida siempre está en inglés, incluso si el usuario escribe en otro idioma.

Compatible con✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/PedroGilo12/claude-skills/tree/HEAD/plugins/firmware-testing/skills/embedded-unit-testing

Installed? Explore more Redacción y edición skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

¿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/.h que 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 .c originales, 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.

FasePaso de AnicheQué haces
0—Detectar entorno → Ruta A (Zephyr), B (ESP-IDF) o C (otro)
11 EntenderRequisito, entradas, salidas de cada función dada
22 Explorar códigoMapear dependencias para aislar con fakes/mocks
33 ParticionesCatálogo numerado de particiones (el paso más importante)
44 LímitesCatálogo numerado de límites (punto on/off)
55 Diseñar casosCombinar particiones/límites, reducir combinaciones
66 AutomatizarEscribir el conjunto en el framework de la ruta, con trazabilidad
7—Ejecutar las herramientas: ejecución + cobertura
87 AumentarIterar 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.

  1. ¿Zephyr? → Ruta A. Señales: west.yml/west.yaml; CMakeLists.txt con find_package(Zephyr ...); prj.conf; testcase.yaml/sample.yaml; tests/; ztest/ZTEST_SUITE. Herramientas: twister + ztest + FFF + cobertura. Lee references/zephyr-path.md.

  2. ¿ESP-IDF? → Ruta B. Señales: CMakeLists.txt con idf_component_register(; sdkconfig/sdkconfig.defaults; uso de $ENV{IDF_PATH}/idf.py; un componente main/. Herramientas: Unity nativo de ESP-IDF (compilación host, objetivo linux) + 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. Lee references/espidf-path.md.

  3. 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:

  1. 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).
  2. 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:

  1. 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) o TEST_IGNORE_MESSAGE("BUG: ...") (Ruta B). Comentario arriba: /* Covers: Pn — BUG: <docs say X; code does Y> */.
  2. 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) o source .venv/bin/activate. El venv suele ser un enlace simbólico: verifica con test -e .venv/bin/activate o ls -laL (que siguen el enlace), nunca solo con ls -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, o west topdir/.west/config) y, para el host, ZEPHYR_TOOLCHAIN_VARIANT=host. Si west no está en el PATH pero 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.sh cuando exista) para la compilación host; de lo contrario, compila con gcc directamente (ver espidf-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 (ver espidf-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, o gcov/gcovr sobre 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:

  1. Ruta elegida y por qué.
  2. Resumen de cobertura (de la última ejecución de la herramienta): lines: X.X%, branches: Y.Y%, functions: Z.Z%.
  3. Estado del objetivo de cobertura: "Alcanzado" / "No alcanzado" (y por qué).
  4. Brechas con justificación (líneas/ramas marcadas como inalcanzables).
  5. Defectos encontrados (saltos por comportamiento documentado vs. real).
  6. Artefactos generados: enlaces a twister-out/coverage/index.html (Ruta A), coverage.html (Ruta B) o el informe que emitió la herramienta (Ruta C).
  7. 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.md
  • references/espidf-path.md
  • references/non-zephyr-path.md

Skills relacionados