Introducción al Framework
Unity es un framework de pruebas unitarias minimalista diseñado específicamente para el lenguaje C. Su objetivo principal es permitir la escritura y ejecución de pruebas en cualquier compilador de C y en cualquier cadena de herramientas para sistemas embebidos, sin importar las restricciones del entorno.
- Ausencia de frameworks estándar: Las toolchains de microcontroladores (MCU) rara vez incluyen soporte para frameworks modernos de C++ como GoogleTest o Catch2, y añadir dependencias complejas suele ser inviable.
- Recursos limitados: La memoria Flash y RAM son escasas, por lo que el framework debe tener una huella mínima y ser altamente configurable para eliminar funciones no utilizadas.
- Integración con sistemas de compilación: Debe adaptarse sin fricciones a Make, CMake, Meson, PlatformIO o scripts personalizados.
La filosofía de diseño se basa en principios estrictos:
- El núcleo consta de un único archivo fuente (
unity.c) y un par de cabeceras (unity.hyunity_internals.h). - La configuración se realiza exclusivamente mediante macros y opciones de compilación, garantizando cero asignación dinámica de memoria en tiempo de ejecución.
- La salida de texto es simple y redirigible, facilitando su análisis por puertos serie, parsers de logs o pipelines de CI/CD.
Arquitectura y Mecanismos Internos
1. Estructura del Proyecto
Para comprender cómo se integra Unity, es útil observar su organización interna.
Unity/
├── src/
│ ├── unity.c # Implementación central: aserciones, salida y control de ejecución
│ ├── unity.h # API pública y macros de aserción (TEST_ASSERT_*)
│ └── unity_internals.h # Estructuras de datos internas e interfaces privadas
├── extras/
│ ├── fixture/ # Extensión para agrupar pruebas (test suites)
│ ├── memory/ # Rastreo de asignaciones de memoria (detección de fugas)
│ ├── bdd/ # Soporte para estilo BDD (Behavior-Driven Development)
│ └── eclipse/ # Integración con IDEs como Eclipse
├── auto/
│ ├── generate_test_runner.rb # Generación automática del ejecutor de pruebas
│ ├── parse_output.rb # Análisis de la salida de las pruebas
│ └── stylize_as_junit.py # Conversión de resultados a formato JUnit (Python)
├── examples/ # Proyectos de demostración con diversas configuraciones
└── test/ # Pruebas de autovalidación del propio framework
Para integrar este framework en un proyecto embebido, generalmente solo se requiere incorporra el directorio src/, compilar unity.c junto con el código de la aplicación y, opcionalmente, utilizar los scripts de auto/ para generar los archivos runner.
2. Desacoplamiento de Macros de Aserción
La API pública expuesta en unity.h utiliza macros para las aserciones:
TEST_ASSERT_EQUAL_INT(expected_val, actual_val);
TEST_ASSERT_FLOAT_WITHIN(tolerance, expected_val, actual_val);
TEST_ASSERT_NOT_NULL(pointer_val);
Estas macros no ejecutan la lógica de comparación directamente, sino que actúan como envoltorios que realizan tres tareas fundamentales:
- Captura del contexto: Utilizan
__LINE__y__FILE__para registrar la ubicación exacta de la llamada. - Definición del formato de salida: Especifican cómo se deben imprimir los valores (por ejemplo, como enteros con signo o en formato hexadecimal).
- Delegación a funciones internas: Redirigen la ejecución a funciones como
UnityAssertEqualIntNumberoUnityAssertFloatsWithin.
3. Estado Global y Control de Ejecución
El ciclo de vida de las pruebas es gestionado por una estructura global UNITY_STORAGE_T Unity, que almacena:
- El nombre, archivo y línea de la prueba en ejecución.
- Contadores globales de pruebas totales, fallidas e ignoradas.
- Banderas de estado para la prueba actual (fallo o ignorada).
UnityBegin: Inicializa las variables globales y prepara la salida.UnityDefaultTestRun: Ejecuta la prueba individual, invocandosetUp, la función de prueba ytearDown, protegidas porTEST_PROTECT().UnityConcludeTest: Evalúa las banderas de estado, actualiza los contadores y emite el resultado de la prueba actual.UnityEnd: Imprime el resumen final y retorna el número de fallos, útil para determinar el código de salida del proceso.
En entornos embebidos, la salida se controla redefiniendo la macro UNITY_OUTPUT_CHAR, permitiendo redirigir los caracteres a una UART, un puerto SWO o un buffer circular en memoria.
4. Configuración y Adaptación de Tipos
El framwork ofrece una amplia gama de directivas #define para adaptar su comportamiento a las limitaciones del hardware:
- Arquitectura de enteros: Macros como
UNITY_SUPPORT_64,UNITY_INT_WIDTHyUNITY_POINTER_WIDTHajustan el tamaño de los tipos base. - Soporte de punto flotente: Se pueden excluir las operaciones con
floatodoublemedianteUNITY_EXCLUDE_FLOATyUNITY_EXCLUDE_DOUBLEpara ahorrar espacio en ROM. - Tipos de contadores:
UNITY_COUNTER_TYPEpermite usar tipos de datos más pequeños (comouint8_t) si el número de pruebas es reducido.
Todas estas decisiones se resuelven en tiempo de compilación, eliminando la sobrecarga de las evaluaciones condicionales en tiempo de ejecución.
Implementación Práctica
A continuación, se demuestra la integración del framework mediante un módulo matemático simple. Supongamos que tenemos una función de multiplicación en math_ops.c:
// math_ops.c
int multiply_values(int factor1, int factor2) {
return factor1 * factor2;
}
1. Integración del Núcleo
Se deben añadir unity.c, unity.h y unity_internals.h al árbol de fuentes del proyecto y asegurar que el archivo fuente sea compilado por el sistema de build (CMake, Make, etc.).
2. Escritura de Casos de Prueba
// test_math_ops.c
#include "unity.h"
#include "math_ops.h"
void setUp(void) {
// Inicialización previa a cada prueba
}
void tearDown(void) {
// Limpieza posterior a cada prueba
}
void test_multiply_should_compute_positive_product(void) {
TEST_ASSERT_EQUAL_INT(20, multiply_values(4, 5));
}
void test_multiply_should_handle_negative_factors(void) {
TEST_ASSERT_EQUAL_INT(-15, multiply_values(3, -5));
}
int main(void) {
UnityBegin("test_math_ops.c");
UnityDefaultTestRun(test_multiply_should_compute_positive_product,
"test_multiply_should_compute_positive_product", __LINE__);
UnityDefaultTestRun(test_multiply_should_handle_negative_factors,
"test_multiply_should_handle_negative_factors", __LINE__);
return UnityEnd();
}
3. Manejo de Fallos en las Aserciones
Si una aserción falla, por ejemplo, al evaluar incorrectamente un producto:
TEST_ASSERT_EQUAL_INT(10, multiply_values(3, -5)); // El resultado real es -15
El mecanismo interno procede de la siguiente manera:
- La macro captura la línea y los valores, pasándolos a
UnityAssertEqualIntNumber. - Al detectar la discrepancia, se invoca
UnityTestResultsFailBeginpara imprimir la cabecera del error (archivo, línea, nombre de la prueba). - Se imprime el valor esperado frente al valor obtenido.
- Se ejecuta
UNITY_FAIL_AND_BAIL, que marca la prueba como fallida y utilizaTEST_ABORT()(generalmente implementado consetjmp/longjmp) para interrumpir la ejecución de la prueba actual.
Este comportamiento de "abortar tras el primer fallo" es crucial en sistemas embebidos para evitar que el hardware entre en estados indefinidos o ejecute código inválido tras un error crítico, garantizando además que tearDown se ejecute para liberar recursos.
4. Agrupación de Pruebas con Fixtures
Para proyectos más extensos, la extensión fixture permite organizar las pruebas en suites, ofreciendo una estructura similar a xUnit:
#include "unity.h"
#include "unity_fixture.h"
#include "math_ops.h"
/* Suite 1: Operaciones básicas */
TEST_GROUP(MathBasic);
TEST_SETUP(MathBasic) {}
TEST_TEAR_DOWN(MathBasic) {}
TEST(MathBasic, MultiplyTwoPositives)
{
TEST_ASSERT_EQUAL_INT(42, multiply_values(6, 7));
}
TEST(MathBasic, MultiplyByZero)
{
TEST_ASSERT_EQUAL_INT(0, multiply_values(99, 0));
}
TEST_GROUP_RUNNER(MathBasic)
{
RUN_TEST_CASE(MathBasic, MultiplyTwoPositives);
RUN_TEST_CASE(MathBasic, MultiplyByZero);
}
/* Suite 2: Casos límite y desbordamiento */
TEST_GROUP(MathEdgeCases);
TEST_SETUP(MathEdgeCases) {}
TEST_TEAR_DOWN(MathEdgeCases) {}
TEST(MathEdgeCases, MultiplyNegativeNumbers)
{
TEST_ASSERT_EQUAL_INT(25, multiply_values(-5, -5));
}
TEST(MathEdgeCases, MultiplyLargeIntegers)
{
TEST_ASSERT_EQUAL_INT(1000000, multiply_values(1000, 1000));
}
TEST_GROUP_RUNNER(MathEdgeCases)
{
RUN_TEST_CASE(MathEdgeCases, MultiplyNegativeNumbers);
RUN_TEST_CASE(MathEdgeCases, MultiplyLargeIntegers);
}
static void ExecuteAllSuites(void)
{
RUN_TEST_GROUP(MathBasic);
RUN_TEST_GROUP(MathEdgeCases);
}
int main(int argc, const char * argv[])
{
return UnityMain(argc, argv, ExecuteAllSuites);
}