cerrar-sesion editar-perfil marker video calendario monitor periodico fax rss twitter facebook google-plus linkedin alarma circulo-derecha abajo derecha izquierda mover-vertical candado usuario email lupa exito mapa email2 telefono etiqueta

400450301. Testado estático del código C++

Escrito por Redacción en Secciones
no hay comentarios Haz tu comentario
Imagen de logotipo de facebook Imagen de logotipo de Twitter Imagen de Logotipo de Google+ Imagen de logotipo de Linkedin

Pongamos que acabamos de escribir una hermosa biblioteca de funciones. Podríamos haber usado cualquier lenguaje de programación, pero la biblioteca les aparece a otros programadores como una secuencia de funciones que pueden ser invocadas por un programa escrito en C (o en lenguajes que cumplen con las convenciones de invocación de función tipo C).

Pero antes de hacerlo llegar a los usuarios (que son ellos mismos programadores de aplicaciones) queremos estar seguros de la corrección de la biblioteca. Por consiguiente, escribimos un programa que usa la biblioteca.

Sin embargo, ese programa presumiblemente no va a invocar a todas las funciones, en todas las formas posibles. Así que, para garantizar una calidad razonable, necesitamos escribir un programa de testado que invoque las funciones de biblioteca, pasando una variedad de posibles valores de parámetros y compruebe si el resultado coincide con los resultados esperados.

El programa de testado se puede escribir en distinto lenguaje de programación, siempre que se intercomunique con las funciones de la biblioteca. Como una función con fallos técnicos podría colapsar un programa, podríamos dividir el programa de testado en varios programas pequeños y lanzarlos de forma secuencial desde un script. Si un programa se colapsa entonces, el script puede continuar con el programa siguiente.

Bibliotecas que hacen surgir la excepción

Asumamos ahora que nuestra biblioteca no es una sencilla biblioteca de funciones C, sino más bien una biblioteca que exporta funciones o clases utilizables sólo mediante código C++. Además, hay que considerar cuestiones relativas a la gestión de excepciones. De hecho, las funciones C++ a testar podrían hacer surgir excepciones, tanto esperadas como inesperadas. La especificación de cada función puede (y debería) declarar qué excepciones pueden surgir mientras se ejecutan dichas funciones.

((Para garantizar la calidad , necesitamos escribir un programa de test que invoque funciones de biblioteca))

Por ejemplo, si invocamos una función pasando como parámetro el nombre de un fichero inexistente, cabría esperar que una excepción de un tipo específico surja mediante esa función. Y si pasamos el nombre de un fichero existente pero ilegible, se espera una excepción de otro tipo. Por último, si se pasa el nombre de un fichero legible, no se espera excepción. Aquí tenemos el código:


try

process_file(“inexistent_file”);

catch (file_not_found_exception &e)

success();

catch (...)

failure(“unexpected exception”);

try

process_file(“read_protected_file”);

catch (file_unreadable_exception &e)

success();

catch (...)

failure(“unexpected exception”);

try

process_file(“readable_file”);

catch (...)

failure(“unexpected exception”);


Para testar correctamente la característica de hacer surgir la excepción de la biblioteca, hay que poner cada invocación de función del programa de testado en un bloque try. Tras las invocaciones para las que se espera una excepción específica (file_not_found_exception o file_unreadable_exception), hay que poner un bloque catch para el tipo de excepción esperado.

Y para cada invocación, hay que poner un bloque catch-all tras el posible bloque específico catch para garantizar que se gestiona cada una de las excepciones inesperadas.

Hay varias plataformas de código abierto —Boost.Test, CppUnit, CppUnitLite, NanoCppUnit, Unit++, y CxxTest, entre otras—que facilitan el desarrollo de programas de testado automático de módulos C++.

Bibliotecas que no pueden ser compiladas

Una cuestión de testado rara vez tenida en cuenta en los libros de texto y en las plataformas de testado es la siguiente: “¿Estamos seguros de que al usar nuestra biblioteca, el programa de aplicación se compilará y enlazará correctamente cuando tenga que hacerlo?”

Cuando usamos bibliotecas escritas en C no es un gran problema – los ficheros de encabezado de bibliotecas definen con claridad qué funciones son exportadas por la biblioteca, y siempre está claro qué función es invocada por una sentencia del programa de testado. Por tanto, en el programa de testado se invoca cada función de biblioteca al menos una vez.

Si podemos compilar y enlazar sin problemas el programa de testado, podemos tener la confianza de que no surgirán problemas de compilación para los usuarios de la biblioteca. Esta es una asunción razonable para desarrolladores que crean bibliotecas para C, Pascal, Fortran y otros lenguajes de acuerdo con los estándares de interfaces.

De hecho, incluso en esos lenguajes, puede haber problemas en lo referente a posibles colisiones de nombres. Con C++, estos problemas se evitan mediante el uso de espacios de nombres. No obstante, surgen algunas complicaciones en C++, principalmente debido a las plantillas y a las funciones sobrecargadas.

Funciones sobrecargadas

En C++, puede haber varias funciones con el mismo nombre – las funciones “sobrecargadas”. Por ejemplo, la Standard Library contiene las funciones double pow(double, double) y float pow(float, float), además de otras versiones sobrecargadas. La primera función realiza la exponenciación de un número doble mediante un exponente doble; la segunda versión hace la misma operación, pero con una base float y un exponente float.

La expresión pow(3., 4.) invoca la función double pow(double, double), en tanto que la expresión pow(3.f, 4.f) invoca la función float pow(float, float).

Pero la expresión pow(3., 4.f) es ilegal, como lo es también la expresión pow(3.f , 4.), porque estas expresiones son consideradas ambiguas por el compilador. La ambigüedad procede del hecho de que no hay una versión sobrecargada que coincida exactamente, y el intento de convertir implícitamente los parámetros para que coincidan con una versión sobrecargada existente conduce a varias funciones sobrecargadas existentes, y el compilador no puede decidir entre las versiones candidatas.

Las normas para ligar una invocación de función a la correspondiente función sobrecargada son bastante complejas, y la situación se ve a menudo complicada aun más por las conversiones implícitas. De hecho, un constructor que obtiene sólo un parámetro y que no está precedido por la clave implícita le permite al compilador realizar una conversión implícita desde el tipo de parámetro al tipo de la clase que contiene dicho constructor.

Un operador de conversión de tipos le permite al compilador realizar una conversión implícita desde la clase que contiene ese operador al tipo declarado como operador.

((Con C++, los problemas se evitan mediante el uso de espacios de nombres. Pero surgen algunas complicaciones en C++))

Con una gramática así, les resulta a veces difícil a los programadores prever qué función será la elegida por el compilador cuando se está compilando una expresión de invocación de función, o si el compilador se rendirá ante una expresión ambigua.

Aunque la ambigüedad está en el código de la aplicación, y no en el código de la biblioteca, una buena biblioteca debería estar diseñada de manera que sea improbable que un código típico de aplicación sea ambiguo.

Por tanto, hay necesidad de testar la usabilidad de una biblioteca, comprobando que se prohíba el uso no razonable y que se permita el razonable. Esto puede hacerse preparando una serie de usos típicos de la biblioteca, algunos razonables y otros no razonables, y verificar que cada uso razonable de la biblioteca se pueda compilar y cada uso no razonable pueda ser detectado por el compilador como error.

Plantillas de función y plantillas de clase

Además de poder crear bibliotecas de función y bibliotecas de clase con C++, podemos crear bibliotecas que contengan plantillas de función y plantillas de clase, dejando la tarea de instanciar dichas plantillas al programa de la aplicación.

A menudo, los parámetros reales de instalación de la plantilla no son previsibles para los desarrolladores de la biblioteca, que sólo pueden especificar en la documentación que esos parámetros deben satisfacer ciertos requisitos. Si se instancian las plantillas con parámetros que no respetan dichos requisitos, el compilador emite mensajes de error.

Por ejemplo, la Standard Library contiene las plantillas de función min y max, y las plantillas de clase vector y list.

La plantilla de función min puede ser instanciada con un tipo numérico o con el tipo string, pero no con el complex type, para ninguna T.

La plantilla de clase vector puede ser instanciada con cualquier tipo si el único objetivo es crear una colección vacía. No obstante, si queremos invocar la función de miembro resize de esa colección, el tipo element tiene que tener un constructor público por defecto o ningún constructor en absoluto.

Bibliotecas como abstracciones: Evitar errores

Cuando se diseña una biblioteca, conviene tener en cuenta no sólo las características que queremos proporcionar para los programadores de la aplicación sino también los errores de programación que queremos evitar.

En realidad, a veces el propósito de una característica específica de una biblioteca no es tanto proporcionar funcionalidad a los usuarios como evitar errores de programación mediante la prohibición de operaciones propensas al error. Por ejemplo, un instante de tiempo y un período de tiempo sin clases distintas de entidades. Podemos añadir dos períodos de tiempo y multiplicar un período de tiempo por un número.

No obstante, no podemos añadir dos instantes de tiempo o multiplicar un instante de tiempo por un número. Podemos sustraer un instante de tiempo de otro instante de tiempo, obteniendo el período de tiempo entre ellos, y podemos sumar un período de tiempo a un instante de tiempo, obteniendo otro instante de tiempo, que sigue al primero por ese período de tiempo.

Si una biblioteca define una clase por instantes de tiempo y otra clase por períodos de tiempo, debería permitir sólo las operaciones que tienen sentido para cada clase. El código de la aplicación que contendría las operaciones no permitidas no debería ser aceptado por un compilador.

Las bibliotecas C++ más avanzadas y sofisticadas definen las plantillas recursivas en un estilo de programación denominado “template metaprogramming.” En este contexto, un error de programación lógico (casi) siempre genera un error de compilación. En general, el mal uso de expresiones prohibidas debería, si es posible, resultar en errores en tiempo de compilación. Con este fin, algunas bibliotecas (Boost, y Loki, por ejemplo) tienen macros definidos para declarar aserciones de tiempo de compilación.

Si un programa que usa bibliotecas Loki contiene la sentencia STATIC_CHECK(3 == 4) cuando se compila, se genera un error. Lo mismo ocurre si se compila un programa que usa bibliotecas Boost y que contiene la sentencia BOOST_STATIC_ASSERT(3 == 4).

Unas sentencias de este tipo, similares a la macro assert de la C Standard Library, le permiten al compilador analizar una expresión constante, y si la expresión sale como falsa, generan un error en tiempo de compilación.

Programas de testado estático

La macro assert es inútil en código de producción. De hecho, además de ser valiosa para la documentación interna, su fin principal es ser evaluada en una ejecución de testado. En la misma lógica, las aserciones estáticas Loki y Boost son de lo más útiles mientras están siendo evaluadas en una compilación de testado.

Pero, ¿qué es una compilación de testado? Es una prueba que consiste en compilar un programa de prueba escrito con el único fin de determinar si la compilación tiene éxito o no.

Vamos a llamar a ese programa, cuya corrección sintáctica hay que comprobar, un “programa de testado estático”. Más aún, vamos a llamar a un programa tradicional, con toda seguridad, correcto desde el punto de vista sintáctico y cuya corrección en tiempo de ejecución hay que comprobar, un “programa de testado dinámico”.

Un programa de testado dinámico contiene una serie de pruebas dinámicas, cada una de las cuales declara el resultado esperado de la evaluación de una expresión mientras se ejecuta el programa. De forma análoga, un programa de testado estático contiene una serie de pruebas, cada una de las cuales declara el resultado esperado de la compilación de una expresión; es decir, si la expresión es sintácticamente correcta (legal) o incorrecta (ilegal).

El testado estático consiste en intentar compilar cada prueba y observar si cualquier error de compilación es resultado.

Se considera que una prueba no es superada, es decir que se ha detectado un error en la biblioteca (o en la prueba), si un snippet de código declarado legal genera algunos errores de compilación, o si un snippet de código declarado ilegal no genera ningún error de compilación.

Y al revés, se considera que una prueba es superada, es decir que el comportamiento ha sido el esperado, si un snippet de código declarado legal no genera ningún error de compilación, o si un snippet de código declarado ilegal sí genera algún error de compilación.

Para el testado de especificación de excepción, es posible especificar que una cierta operación debería causar el surgimiento de una excepción específica. A su vez, no es factible especificar que la compilación de una cierta sentencia debería causar un error de compilación específico, ya que los errores de compilación no están estandarizados, y varían incluso entre las versiones del mismo compilador. Por lo tanto, nos sentiremos satisfechos con distinguirlos entre código legal y código ilegal.

Marcadores Snippet de código

Supongamos que queremos comprobar la legalidad de 10 sentencias. Si las ponemos a todas en el mismo programa, nos enfrentamos a la siguiente dificultad: Si se puede compilar el programa sin errores, entonces inferimos que las 10 sentencias son sintácticamente correctas.

Pero si la compilación genera errores, es difícil asegurarse de cuál de las sentencias es la sintácticamente incorrecta. Podría ser incluso que todas las sentencias sean correctas cuando se consideran una a una, pero su coexistencia en el mismo programa es incorrecta.

Un método mejor sería:
– Preparar el código fuente de un programa sintácticamente correcto que no contiene las sentencias a comprobar.
– Crear 10 copias de este programa, insertando en cada copia una de las sentencias a comprobar, obteniendo de esta forma 10 programas diferentes.
– Compilar de forma separada los 10 programas. Las sentencias erróneas son las que hicieron que el programa original no fuera ya adecuado para la compilación.

El método es efectivo, pero laborioso para los usuarios. Por tanto, conviene automatizarlo.

Para tales fines, podemos insertar las 10 sentencias en el mismo programa, y enviar el código fuente a una utilidad que genere las 10 versiones del código fuente, una para cada sentencia a testear. Por supuesto, para dejar que la utilidad sepa qué elementos tiene que testear, dichos elementos tendrían que estar marcados de la forma adecuada. De hecho, generalmente no son sentencias sencillas sino snippets de código que contienen una o más sentencias.

Además, este marcado tiene que especificar si los snippets de código señalados son declarados legales o ilegales. El Listado número 1 es un ejemplo de programa de comprobación estática que contiene cuatro snippets de código. Se utilizan tres marcadores (no está permitido el anidado):
– @LEGAL marca el comienzo de un snippet de código declarado como legal.
– @ILLEGAL marca el comienzo de un snippet de código declarado como ilegal.
– @END marca el final del snippet de código actual.

El programa ejemplo incluye sentencias comunes a todo test. Después de estas sentencias, hay un bloque de dos sentencias declaradas legales, la declaración de un vector de números complejos, y una invocación a la función min con dos números enteros como parámetros. Estas sentencias son realmente legales.

Después, hay un bloque de dos sentencias declaradas como ilegales. De hecho, sólo la segunda sentencia es ilegal, ya que intenta clasificar números complejos. La variable v es declarada dos veces. Eso no es un error, ya que cuando el primer snippet de código está activo, el segundo snippet de código está inactivo, y vice-versa; por tanto, las dos declaraciones no co-existen.

A continuación hay un snippet de código que contiene una única sentencia que es declarada ilegal. Es realmente ilegal porque intenta computar el menor entre un valor int y un valor double.

Por último, hay un snippet de código de siete estamentos, declarado legal. De hecho, este snippet de código no contiene errores de sintaxis, pero la segunda línea imprime el valor de una variable que no ha recibido nunca un valor. En una situación así, muchos compiladores emiten avisos si se han fijado las necesarias opciones de compilación.

Ello hace surgir otra cuestión: Si un snippet de código declarado legal hace que el compilador emita un aviso, ¿hay que considerar que la prueba ha tenido éxito o ha sido fallida? Y si un snippet de código declarado ilegal hace que el compilador emita un aviso pero no errores, ¿hay que considerar que la prueba ha tenido éxito o ha sido fallida?

De alguna forma, en ambos casos la prueba ha tenido parcialmente éxito. En consecuencia, hay un tercer resultado posible para el test: Además de fallo y éxito, puede haber un aviso.

Una utilidad de comprobación estática

Voy a presentar aquí una utilidad que implementa automatización de testado estático. La utilidad es un programa estándar C++ que extrae de la línea de comandos las rutas o ficheros que contienen programas de testado estático, y accede a algunas variables de entorno de configuración. (el código fuente está disponible en www.ddj.com/code/.)

El programa lee los ficheros a procesar, y para cada uno (y para cada snippet marcado y contenido en el fichero), genera un nuevo fichero fuente cuya corrección sintáctica es comprobada entonces.

Para comprobar la validez sintáctica de una sentencia, la técnica más eficaz sería usar una biblioteca que proporcione un servicio de compilación o un servicio de análisis estático pero no existe ese tipo de bibliotecas para C++ (o tienen poca popularidad). Alternativamente, se puede lanzar un compilador.

Si una compilación tiene éxito, el proceso normalmente establece el código de retorno en cero; si falla, establece el código de retorno en otro valor. Así pues, basta con ejecutar el compilador, pasándole las opciones deseadas y el código fuente a comprobar, esperar a que la compilación se complete, y comprobar el código de retorno del proceso. Eso es lo que hace la utilidad.

Algunos compiladores permiten una opción que nos deja realizar sólo una comprobación de sintaxis en el código sin generar el código ejecutable, acelerando así la compilación. Se puede obtener una mayor aceleración deshabilitando la optimización del código. Aún así, algunos avisos no son producidos si se realiza una comprobación sencilla de la sintaxis o si las optimizaciones del código están deshabilitadas.

Por tanto, no es aconsejable generar realmente el programa ejecutable optimizado, incluso si no se ejecuta nunca. Además, no hay una técnica estándar para lanzar un compilador – cada compilador tiene sus opciones incompatibles. Por tanto, he colocado las dependencias del compilador y las plataformas a las variables del entorno.

Incluido con el paquete de descarga de la utilidad hay dos scripts —uno para el intérprete de comandos Windows (CMD.EXE) que testea el programa con Visual C++ y los compiladores GCC, y el otro para los caparazones de UNIX/Linux que testea el mismo programa con el compilador GCC. La Figura 1 es el resultado de la ejecución del script para Windows. DDJ

Carlo es desarrollador de software CAD/CAM en Bergamo, Italia.

Etiquetas

Noticias relacionadas

Comentarios

No hay comentarios.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

Debes haber iniciado sesión para comentar una noticia.