Pruebas unitarias para sistemas embebidos (1)

(Draft) Pruebas unitarias para sistemas embebidos, primera de varias partes

En esta serie explicaré la utilización de las pruebas unitarias (UT) en sistemas embebidos. Las UT se originaron y se usan para sistemas de muy alto nivel, en diversos lenguajes como Java, C y C#, entre otros. El reto es incorporar esta herramienta en la programación de sistemas embebidos.

Antes de entrar en detalles, es necesario explicar lo que son las UT en los sistemas de alto nivel para después adaptarlos a nuestras necesidades. En esta primera entrega se utilizará el framework CUnit y el compilador Mingw para los ejemplos. Una vez entendida la razón y utilidad de las UT, se procederá a verlos en los SE.

En la segunda entrega se verá cómo utilizar las UT en sistemas embebidos reales (con los periféricos, funciones de tratamiento de cadenas, etc).

Pruebas unitarias (UT, unit test)

También conocido como Test Driven Development (TDD), o desarrollo dirigido por pruebas, las UT son un paradigma que establece que primero se escriben las pruebas y luego el código que hace que esas pruebas sean exitosas. Esto es algo diferente a la forma en que la mayoría estamos acostumbrados a codificar, y es similar a la transición de la programación estructurada a la programación con objetos; pero con un poco de práctica y paciencia se puede lograr hacer el cambio. Cabe destacar que las UT se pueden implementar con lenguajes estructurados o de objetos, e inclusive en ensamblador.

El término “unit” indica que el software debe ser codificado en “unidades” (o “módulos”; en mi caso personal prefiero el segundo). Un módulo tiene entradas, salidas y un propósito específico; el propósito se refiere a la tarea que se tiene que llevar a cabo. Las entradas van a alimentar con datos al módulo, el módulo va a procesar esos datos, y las salidas van a entregar el resultado. Es en este punto donde el programador se concentra en qué va a hacer el módulo, ya que  aún no interesa en cómo lo va a hacer. Esta idea es importante a la hora de comenzar a escribir las pruebas.

Un módulo puede ser algo tan simple como una función que hace la suma entre dos números, o algo más complicado como parsear un archivo en busca de direcciones de internet, y tal módulo podría tener muchas funciones y estar organizado en varios archivos. Sin embargo, para ejemplificar el uso de las UT tomaré el caso más simple: un módulo que sume aritméticamente dos números enteros. Después de entender el procedimiento general, será muy fácil extenderlo a situaciones más complicadas.

Framework

Como se mencionó, se escribirá un módulo que devuelva la suma de dos números enteros. Pero antes de comenzar se requiere contar con la infraestructura para realizar las pruebas. El marco de trabajo se puede construir en forma manual <link>, o utilizar alguno de los ya existentes. El más popular es el llamado jUnit, pero ese sólo sirve para Java, así que no nos interesa. Otro marco de trabajo popular es el  CppUnit, pero este tampoco nos sirve porque es para C++, y nosotros vamos a programar las UT en C. Existen diferentes frameworks para C, y en lo particular utilizo el llamado CUnit; sin embargo, es buena idea utilizar dos o tres de ellos, y ver con cual nos adaptamos mejor, ya que en el fondo la filosofía es la misma, lo que cambia va a ser la forma de implementarlo.

En <dirección> se encuentra un tutorial para CUnit, y en <dir> las instrucciones para utilizarlo en DevCpp.

Ejemplo

Un módulo que sume dos enteros. Para mantener simple a nuestro  ejemplo asumiremos que será una función en C. Esta función recibirá dos números, a y b, y devolverá el resultado a+b. Como mencioné más arriba, aquí es importante pensar en qué va a hacer la función, no cómo va a hacerlo. Esto lo resalto ya que el primer prototipo de la función no devolverá nada (nada útil, como se verá más adelante).

1. Interfaz

El primer paso es pensar en la interfaz, es decir, cómo va interactuar el usuario con el módulo; en nuestro caso queda de la siguiente manera:

int suma(int a, int b){return 0;}

A esto me refería con “nada útil”. Escogí el cero ya que es el nulo aritmético; si se tratara de una multiplicación, entonces hubiera escogido el uno. Si el valor devuelto fuera un puntero, entonces dicho valor devuelto tendría que ser NULL; como se ve, el nulo va a depender del contexto, pero siempre va a ser necesario regresar algo. Más adelante se escribirá el código de la función.

2. Pruebas

El siguiente paso es escoger las pruebas. Existe una metodología para hacerlo… .  Por la naturaleza del módulo que estamos escribiendo, las pruebas más lógicas son las reglas de la suma:

  • nulo+nulo->nulo,
  • valor+nulo->valor,
  • -valor+nulo-> -valor,
  • valorA+valorB->valorC,
  • valorB+valorA->valorC,
  • etc.

Todas estas pruebas estarán contenidas en lo que se conoce como “suite” (en la terminología de CUnit). Piense en la suite como el conjunto de pruebas relacionadas con el módulo. Un proyecto puede contener varias suites (extendiendo nuestro ejemplo: para la suma, para la multiplicación, etc). Al conjunto de suites se le conoce como “batería de pruebas”.

La suite de pruebas se debe parecer a esto:

void test_suma()
{
 CU_ASSERT(suma(0,0)==0);
 CU_ASSERT(suma(0,2)==2);
 CU_ASSERT(suma(0,-2)==-2);
 CU_ASSERT(suma(2,6)==8);
 CU_ASSERT(suma(6,2)==8);
/* Estas pruebas deben fallar!! */
 CU_ASSERT(suma(2,2)==10);
 CU_ASSERT_EQUAL(9,suma(3,2));
}

Como se observa, se están poniendo a pruebas las leyes de la suma aritmética.

3. Inicializar el framework

Una vez que ya se tiene lista el conjunto de pruebas,  hay que inicializar el framework y agregar nuestra suite a una batería de pruebas. Se pueden tener varias baterías de pruebas, según lo demande el proyecto.  En nuestro caso sólo es una.

int main(int argc, char *argv[])
{
 CU_pSuite psuite=NULL;
 if(CU_initialize_registry()!=CUE_SUCCESS) return CU_get_error();

 /*
 * Agrega una batería de pruebas
 */
 psuite=CU_add_suite("suite1", init_suite, clean_suite);
 if(psuite==NULL){
   CU_cleanup_registry();
   return CU_get_error();
 }
 /*
 * Agrega una prueba a la batería
 */
 if(CU_add_test(psuite,"PRUEBA DE suma()",test_suma)==NULL){
   CU_cleanup_registry();
   return CU_get_error();
 }

 CU_basic_set_mode(CU_BRM_VERBOSE);
 CU_basic_run_tests();
 CU_cleanup_registry();

 system("PAUSE");
 return CU_get_error();
}

La inicialización del framework se da de la siguiente manera:

  • La línea 03 inicializa a CUnit.
  • La línea 08 crea una batería de pruebas.
  • La línea 16 agrega la suite de pruebas del módulo de la suma a la batería de pruebas.
  • La línea 21 establece el modo ‘verbose’, es decir, al ejecutar las pruebas, CUnit va a imprimir más información de la normal.

La función CU_basic_run_tests() ejecuta la batería de pruebas, y la línea 23 limpia el registro (devuelve la memoria utilizada, entre otras cosas) una vez que se terminó con las pruebas.

Resultados (1ra parte)

En este punto, cuando ejecutemos nuestras pruebas, la mayoría de ellas van a fallar (excepto la primera, ¿porqué?). Fallan porque ¡la función suma() no tiene código (aún)! Recuerden que sólo devuelve el nulo aritmético (por eso la primera prueba no falla). Sin embargo, ya tenemos listo y funcionando nuestra infraestructura para hacer las pruebas unitarias. (Nota: dado que los módulos devuelven nulo, y en caso de que estemos usando aserciones, sería buena idea desactivar nuestros ASSERT ya que se van a disparar, y en este momento no queremos eso).

Escribir el código del módulo

Ahora sí, ya con nuestro framework trabajando, estamos en condiciones de sentarnos a escribir el código de nuestro módulo suma. Como nada en la vida es perfecto, vamos a introducir un error a propósito para simular lo que sucede cuando ya estamos seguros de que el módulo va a funcionar, pero al final no funcionó debido a errores de codificación o de lógica. También en este paso es conveniente reactivar nuestras ASSERT, así las pruebas serán más robustas:

int suma(int a, int b)
{
 return (a-b);
}

Ups, no nos dimos cuenta y sin querer cambiamos un signo de ‘+’ por uno de ‘-‘. Aunque simplista, esto demuestra lo que sucede cuando se introducen errores en los módulos, y por tanto, estos no van a funcionar como se espera.

Resultados (2da parte)

Si ejecutamos nuestras pruebas nos vamos a dar cuenta que algo no está bien. Aquí está la salida.

Depuración

Luego de esa desagradable sorpresa,  es momento de revisar el código para investigar lo que estuvo mal. Después de pasar un par de horas estudiando el código, vemos que el error está en la línea

 return (a-b);

y la cambiamos a la que es correcta:

return (a+b);

Resultados (3ra parte)

Volvemos a correr las pruebas. Ahora sí, ¡nuestro módulo suma acaba de pasar todas las pruebas!. Aquí está la salida:

Las pruebas resultaron exitosas

Noten que las pruebas que queríamos que fallaran (las dos últimas de nuestra suite) también fallaron, y eso es bueno, porque quiere decir que nuestro módulo no hará lo que no debe.

Este mismo procedimiento se repite con todos los módulos (o unidades) del proyecto.

Una ventaja de esta forma de programar es que después de un tiempo podemos tomar un módulo y hacerle mejoras, tales como código más corto, o más rápido, o con menor uso de memoria, etc.  En la programación tradicional haríamos un par de pruebas y lo etiquetaríamos como de producción. Pero en la TDD, dado que las pruebas ya están escritas y listas para usarse, entonces lo que hacemos es volver a correrlas en nuestro módulo suma, y con ello tendríamos la seguridad de que los cambios introducidos no cambiaron el comportamiento del módulo.

Espero que esta primera parte haya servido de introducción a las pruebas unitarias, y que sirva como puente para ver cómo se pueden implementar en los sistemas embebidos.

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s