Symfony 1.1, la guía definitiva

15.1. Automatización de pruebas

Cualquier programador con experiencia en el desarrollo de aplicaciones web conoce de sobra el esfuerzo que supone probar correctamente la aplicación. Crear casos de prueba, ejecutarlos y analizar sus resultados es una tarea tediosa. Además, es habitual que los requisitos de la aplicación varíen constantemente, con el consiguiente aumento del número de versiones de la aplicación y la refactorización continua del código. En este contexto, es muy probable que aparezcan nuevos errores.

Este es el motivo por el que la automatización de pruebas es una recomendación, aunque no una obligación, útil para crear un entorno de desarrollo satisfactorio. Los conjuntos de casos de prueba garantizan que la aplicación hace lo que se supone que debe hacer. Incluso cuando el código interno de la aplicación cambia constantemente, las pruebas automatizadas permiten garantizar que los cambios no introducen incompatibilidades en el funcionamiento de la aplicación. Además, este tipo de pruebas obligan a los programadores a crear pruebas en un formato estandarizado y muy rígido que pueda ser procesado por un framework de pruebas.

En ocasiones, las pruebas automatizadas pueden reemplazar la documentación técnica de la aplicación, ya que ilustran de forma clara el funcionamiento de la aplicación. Un buen conjunto de pruebas muestra la salida que produce la aplicación para una serie de entradas de prueba, por lo que es suficiente para entender el propósito de cada método.

Symfony aplica este principio a su propio código. El código interno del framework se valida mediante pruebas automáticas. Estas pruebas unitarias y funcionales no se incluyen en la distribución estándar de Symfony, pero se pueden descargar directamente desde el repositorio de Subversion y se pueden acceder online en http://trac.symfony-project.org/browser/branches/1.1/test

15.1.1. Pruebas unitarias y funcionales

Las pruebas unitarias aseguran que un único componente de la aplicación produce una salida correcta para una determinada entrada. Este tipo de pruebas validan la forma en la que las funciones y métodos trabajan en cada caso particular. Las pruebas unitarias se encargan de un único caso cada vez, lo que significa que un único método puede necesitar varias pruebas unitarias si su funcionamiento varía en función del contexto.

Las pruebas funcionales no solo validan la transformación de una entrada en una salida, sino que validan una característica completa. Un sistema de cache por ejemplo solamente puede ser validado por una prueba funcional, ya que comprende más de 1 solo paso: la primera vez que se solicita una página, se produce su código; la segunda vez, se obtiene directamente de la cache. De modo que las pruebas funcionales validan procesos y requieren de un escenario. En Symfony, se deberían crear pruebas funcionales para todas las acciones.

Para las interacciones más complejas, estos 2 tipos de pruebas no son suficientes. Las interacciones de Ajax, por ejemplo, requieren de un navegador web que ejecute código JavaScript, por lo que es necesaria una herramienta externa para la automatización de las pruebas. Además, los efectos visuales solamente pueden ser validados por una persona.

Si las pruebas automatizadas van a validar una aplicación compleja, probablemente sea necesario el uso combinado de estos 3 tipos de pruebas. Como recomendación final, es aconsejable crear pruebas sencillas y fáciles de entender.

Nota Las pruebas automatizadas comparan un resultado con la salida esperada para ese método. En otras palabras, evalúan "asertos" (del inglés, "assertions", que son expresiones del tipo $a == 2. El valor de salida de un aserto es true o false, lo que determina si la prueba tiene éxito o falla. La palabra "aserto" es de uso común en las técnicas de automatización de pruebas.

15.1.2. Desarrollo basado en pruebas

La metodología conocida como TDD o "desarrollo basado en pruebas" "test-driven development") establece que las pruebas se escriben antes que el código de la aplicación. Crear las pruebas antes que el código, ayuda a pensar y centrarse en el funcionamiento de un método antes de programarlo. Se trata de una buena práctica que también recomiendan otras metodologías como XP "Extreme Programming"). Además, es un hecho innegable que si no se escriben las pruebas antes, se acaba sin escribirlas nunca.

En el siguiente ejemplo se supone que se quiere desarrollar una función elimine los caracteres problemáticos de una cadena de texto. La función elimina todos los espacios en blanco del principio y del final de la cadena; además, reemplaza todos los caracteres que no son alfanuméricos por guiones bajos y transforma todas las mayúsculas en minúsculas. En el desarrollo basado en pruebas, en primer lugar se piensa en todos los posibles casos de funcionamiento de este método y se elabora una serie de entradas de prueba junto con el resultado esperado para cada una, como se muestra en la tabla 15-1.

Tabla 15-1 - Lista de casos de prueba para la función que elimina caracteres problemáticos

Dato de entrada Resultado esperado
" valor " "valor"
"valor otrovalor" "valor_otrovalor"
"-)valor:..=otrovalor?" "__valor____otrovalor_"
"OtroValor" "otrovalor"
"¡Un valor y otro valor!" "_un_valor_y_otro_valor_"

A continuación, se crearían las pruebas unitarias, se ejecutarían y todas fallarían. Después, se escribe el código necesario para realizar correctamente el primer caso y se vuelven a pasar todas las pruebas. En esta ocasión, la primera prueba no fallaría. Así se seguiría desarrollando el código del método completo hasta que todas las pruebas se pasen correctamente.

Una aplicación desarrollada con la metodología basada en pruebas, acaba teniendo tanto código para pruebas como código para aplicación. Por tanto, las pruebas deberían ser sencillas para no perder el tiempo arreglando los problemas con el código de las pruebas.

Nota Refactorizar el código de un método puede crear errores que antes no existían. Esta es otra razón por la que es una buena idea pasar todas las pruebas creadas antes de instalar una nueva versión de la aplicación en el servidor de producción. Esta técnica se conoce como "regression testing".

15.1.3. El framework de pruebas Lime

En el ámbito de PHP existen muchos frameworks para crear pruebas unitarias, siendo los más conocidos PhpUnit y SimpleTest. Symfony incluye su propio frameowrk llamado Lime. Se basa en la librería Test::More de Perl y es compatible con TAP, lo que significa que los resultados de las pruebas se muestran con el formato definido en el "Test Anything Protocol", creado para facilitar la lectura de los resultados de las pruebas.

Lime proporciona el soporte para las pruebas unitarias, es más eficiente que otros frameworks de pruebas de PHP y tiene las siguientes ventajas:

  • Ejecuta los archivos de prueba en un entorno independiente para evitar interferencias entre las diferentes pruebas. No todos los frameworks de pruebas garantizan un entorno de ejecución "limpio" para cada prueba.
  • Las pruebas de Lime son fáciles de leer y sus resultados también lo son. En los sistemas operativos que lo soportan, los resultados de Lime utilizan diferentes colores para mostrar de forma clara la información más importante.
  • Symfony utiliza Lime para sus propias pruebas y su "regression testing", por lo que el código fuente de Symfony incluye muchos ejemplos reales de pruebas unitarias y funcionales.
  • El núcleo de Lime se valida mediante pruebas unitarias.
  • Está escrito con PHP, es muy rápido y está bien diseñado internamente. Consta úicamente de un archivo, llamado lime.php, y no tiene ninguna dependencia.

Las pruebas que se muestran en las secciones siguientes utilizan la sintaxis de Lime, por lo que funcionan directamente en cualquier instalación de Symfony.

Nota Las pruebas unitarias y funcionales no están pensadas para lanzarlas en un servidor de producción. Se trata de herramientas para el programador, por lo que solamente deberían ejecutarse en la máquina de desarrollo del programador y no en un servidor de producción.