Diseño ágil con TDD

4.1. Terminología en la comunidad TDD

Desde el aspecto potestad, es decir, mirando los tests según a quién le pertenecen, distinguimos entre tests escritos por desarrolladores y tests escritos por el Dueño del Producto. Recordemos que el Dueño del Producto es el analista de negocio o bien el propio cliente. Lo ideal es que el analista de negocio ayude al cliente a escribir los tests para asegurarse de que las afirmaciones están totalmente libres de ambigüedad.

Los tests que pertenecen al Dueño del Producto se llaman tests de cliente o de aceptación. Charlie Poole prefiere llamarles tests de cliente ya que por aceptación se podría entender que se escriben al final cuando, realmente, no tiene que ser así. De hecho, en TDD partimos de tests de aceptación (ATDD) para conectar requerimientos con implementación, o sea, que los escribimos antes que nada. Cuando se escribe el código que permite ejecutar este test, y se ejecuta positiva mente, se entiende que el cliente acepta el resultado. Por esto se habla de aceptación. Y también por esto es un término provocador, al haber clientes que niegan que un test de aceptación positivo signifique que aceptan esa parte del producto.

Nosotros hablaremos de aceptación porque se usa más en la literatura que test de cliente, aunque convendrá recordar lo peligrosa que puede llegar a ser esta denominación. En el siguiente diagrama se muestra la clasificación de los tests típica de un entorno ATDD/TDD. A la izquierda, se agrupan los tests que pertenecen a desarrolladores y, a la derecha, los que pertenecen al Dueño del Producto. A su vez, algunos tipos de tests contienen a otros.

Clasificación de los test ATDD/TDD

Figura 4.1 Clasificación de los test ATDD/TDD

4.1.1.  Tests de Aceptación

¿Cómo es un test de aceptación? Es un test que permite comprobar que el software cumple con un requisito de negocio. Como se vio en el capítulo de ATDD, un test de aceptación es un ejemplo escrito con el lenguaje del cliente pero que puede ser ejecutado por la máquina. Recordemos algunos ejemplos:

  • El producto X con precio 50 euros tiene un precio final de 55 euros después de aplicar el impuesto Z
  • Si el paciente nació el 1 de junio de 1981, su edad es de 28 años en agosto de 2009

¿Los tests de aceptación no usan la interfaz de usuario del programa? Podría ser que sí, pero en la mayoría de los casos la respuesta debe ser no. Los tests de carga y de rendimiento son de aceptación cuando el cliente los considera requisitos de negocio. Si el cliente no los requiere, serán tests de desarrollo.

4.1.2.  Tests Funcionales

Todos los tests son en realidad funcionales, puesto que todos ejercitan alguna función del SUT5, aunque en el nivel más elemental sea un método de una clase. No obstante, cuando se habla del aspecto funcional, se distingue entre test funcional y test no funcional.

Un test funcional es un subconjunto de los tests de aceptación. Es decir, comprueban alguna funcionalidad con valor de negocio. Hasta ahora, todos los tests de aceptación que hemos visto son tests funcionales. Los tests de aceptación tienen un ámbito mayor porque hay requerimientos de negocio que hablan de tiempos de respuesta, capacidad de carga de la aplicación, etc; cuestiones que van más allá de la funcionalidad.

Un test funcional es un test de aceptación pero, uno de aceptación, no tiene por qué ser funcional.

4.1.3. Tests de Sistema

Es el mayor de los tests de integración, ya que integra varias partes del sistema. Se trata de un test que puede ir, incluso, de extremo a extremo de la aplicación o del sistema. Se habla de sistema porque es un término más general que aplicación, pero no se refiere a administración de sistemas, no es que estemos probando el servidor web o el servidor SMTP aunque, tales servicios, podrían ser una parte de nuestro sistema.

Así pues, un test del sistema se ejercita tal cual lo haría el usuario humano, usando los mismos puntos de entrada (aquí sí es la interfaz gráfica) y llegando a modificar la base de datos o lo que haya en el otro extremo.

¿Cómo se puede automatizar el uso de la interfaz de usuario y validar que funciona? Hay software que permite hacerlo. Por ejemplo, si la interfaz de usuario es web, el plugin Selenium para el navegador Mozilla Firefox nos permite registrar nuestra actividad en una página web como si estuviéramos grabando un vídeo para luego reproducir la secuencia automáticamente y detectar cambios en la respuesta del sitio web.

Pongamos que grabo la forma en que relleno un formulario con una dirección de correo electrónico incorrecta para que el sitio web me envíe un mensaje de error. Cada vez que quiera volver a comprobar que el sitio web responde igual ante esa entrada,sólo tengo que ejecutar el test generado por Selenium.

Hay herramientas que permiten hacer lo mismo mediante programación: nos dan una API para seleccionar controles gráficos, y accionarlos desde código fuente, comprobando el estado de la ejecución con sentencias condicionales o asertivas. El propio Selenium lo permite.

Una de las herramientas más populares es Watir para Ruby y sus versiones para otros lenguajes de programación (Watin para .Net). Para aplicaciones escritas con el framework Django (Python), se utiliza el cliente web. Para aplicaciones de escritorio, hay frameworks específicos como UIAutomation o NUnitForms que también permiten manipular la interfaz gráfica desde código.

Existen muchas formas de probar un sistema. Supongamos que hemos implementado un servidor web ligero y queremos validar que, cada vez que alguien accede a una página, se registra su dirección IP en un fichero de registro (log). Podríamos hacer un script con algún comando que se conecte a una URL del servidor, al estilo de Wget desde la misma máquina y después buscar la IP 127.0.0.1 en el fichero de log con Grep. Sirva el ejemplo para recalcar que no hay una sola herramienta ni forma de escribir tests de sistema, más bien depende de cada sistema.

Los tests de sistema son muy frágiles en el sentido de que cualquier cambio en cualquiera de las partes que componen el sistema, puede romperlos. No es recomendable escribir un gran número de ellos por su fragilidad. Si la cobertura de otros tipos de tests de granularidad más fina, como por ejemplo los unitarios, es amplia, la probabilidad de que los errores sólo se detecten con tests de sistema es muy baja.

O sea, que si hemos ido haciendo TDD, no es productivo escribir tests de sistema para todas las posibles formas de uso del sistema, ya que esta redundancia se traduce en un aumento del costo de mantenimiento de los tests.

Por el contrario, si no tenemos escrito absolutamente ningún tipo de test, blindar la aplicación con tests de sistema será el paso más recomendable antes de hacer modificaciones en el código fuente. Luego, cuando ya hubiesen tests unitarios para los nuevos cambios introducidos, se podrían ir desechando tests de sistema.

¿Por qué se les llama tests de aceptación y tests funcionales a los tests de sistema? En mi opinión, es un error. Un test funcional es una frase escrita en lenguaje natural que utiliza el sistema para ejecutarse. En el caso de probar que una dirección de email es incorrecta, el test utilizará la parte del sistema que valida emails y devuelve mensajes de respuesta.

Sin embargo, el requisito de negocio no debe entrar en cómo es el diseño de la interfaz de usuario. Por tanto, el test funcional no entraría a ejecutar el sistema desde el extremo de entrada que usa el usuario (la GUI), sino desde el que necesita para validar el requisito funcional. Si la mayoría de los criterios de aceptación se validan mediante tests funcionales, tan sólo nos harán falta unos pocos tests de sistema para comprobar que la GUI está bien conectada a la lógica de negocio. Esto hará que nuestros tests sean menos frágiles y estaremos alcanzando el mismo nivel de cobertura de posibles errores.

En la documentación de algunos frameworks, llaman test unitarios a tests que en verdad son de integración y, llaman tests funcionales, a tests que son de sistema. Llamar test funcional a un test de sistema no es un problema siempre que adoptemos esa convención en todo el equipo y todo el mundo sepa para qué es cada test.

En casos puntuales, un requisito de negocio podría involucrar la GUI, tal como pasa con el cliente de Gmail del iPhone por ejemplo. Está claro que el negocio en ese proyecto está directamente relacionado con la propia GUI. En ese caso, el test de sistema sería también un test funcional.

4.1.4.  Tests Unitarios

Son los tests más importantes para el practicante TDD, los ineludibles. Cada test unitario o test unidad (unit test en inglés) es un paso que andamos en el camino de la implementación del software. Todo test unitario debe ser:

  • Atómico
  • Independiente
  • Inocuo
  • Rápido

Si no cumple estas premisas entonces no es un test unitario, aunque se ejecute con una herramienta tipo xUnit.

Atómico significa que el test prueba la mínima cantidad de funcionalidad posible. Esto es, probará un solo comportamiento de un método de una clase. El mismo método puede presentar distintas respuestas ante distintas entradas o distinto contexto. El test unitario se ocupará exclusivamente de uno de esos comportamientos, es decir, de un único camino de ejecución.

A veces, la llamada al método provoca que internamente se invoque a otros métodos; cuando esto ocurre, decimos que el test tiene menor granularidad, o que es menos fino. Lo ideal es que los tests unitarios ataquen a métodos lo más planos posibles, es decir, que prueben lo que es indivisible. La razón es que un test atómico nos evita tener que usar el depurador para encontrar un defecto en el SUT, puesto que su causa será muy evidente.

Como veremos en la parte práctica, hay veces que vale la pena ser menos estrictos con la atomicidad del test, para evitar abusar de los dobles de prueba.

Independiente significa que un test no puede depender de otros para producir un resultado satisfactorio. No puede ser parte de una secuencia de tests que se deba ejecutar en un determinado orden. Debe funcionar siempre igual independientemente de que se ejecuten otros tests o no.

Inocuo significa que no altera el estado del sistema. Al ejecutarlo una vez, produce exactamente el mismo resultado que al ejecutarlo veinte veces. No altera la base de datos, ni envía emails ni crea ficheros, ni los borra. Es como si no se hubiera ejecutado.

Rápido tiene que ser porque ejecutamos un gran número de tests cada pocos minutos y se ha demostrado que tener que esperar unos cuantos segundos cada rato, resulta muy improductivo. Un sólo test tendría que ejecutarse en una pequeña fracción de segundo. La rapidez es tan importante que Kent Beck ha desarrollado recientemente una herramienta que ejecuta los tests desde el IDE Eclipse mientras escribimos código, para evitar dejar de trabajar en código mientras esperamos por el resultado de la ejecución. Se llama JUnit Max. Olof Bjarnason ha escrito otra similar y libre para Python.

Para conseguir cumplir estos requisitos, un test unitario aisla la parte del SUT que necesita ejercitar de tal manera que el resto está inactivo durante la ejecución. Hay principalmente dos formas de validar el resultado de la ejecución del test: validación del estado y validación de la interacción, o del comportamiento. En los siguientes capítulos los veremos en detalle con ejemplos de código.

Los desarrolladores utilizamos los tests unitarios para asegurarnos de que el código funciona como esperamos que funcione, al igual que el cliente usa los tests de cliente para asegurarse que los requisitos de negocio se alcancen como se espera que lo hagan.

F.I.R.S.T

Como los acrónimos no dejan de estar de moda, cabe destacar que las características de los tests unitarios también se agrupan bajo las siglas F.I.R.S.T que vienen de: Fast, Independent, Repeatable, Small y Transparent. Repetible encaja con inocuo, pequeño caza con atómico y transparente quiere decir que el test debe comunicar perfectamente la intención del autor.

4.1.5. Tests de Integración

Por último, los tests de integración son la pieza del puzzle que nos faltaba para cubrir el hueco entre los tests unitarios y los de sistema. Los tests de integración se pueden ver como tests de sistema pequeños.

Típicamente, también se escriben usando herramientas xUnit y tienen un aspecto parecido a los tests unitarios, sólo que estos pueden romper las reglas. Como su nombre indica, integración significa que ayuda a unir distintas partes del sistema. Un test de integración puede escribir y leer de base de datos para comprobar que, efectivamente, la lógica de negocio entiende datos reales. Es el complemento a los tests unitarios, donde habíamos falseado el acceso a datos para limitarnos a trabajar con la lógica de manera aislada. Un test de integración podría ser aquel que ejecuta la capa de negocio y después consulta la base de datos para afirmar que todo el proceso, desde negocio hacia abajo, fue bien. Son, por tanto, de granularidad más gruesa y más frágiles que los tests unitarios, con lo que el número de tests de integración tiende a ser menor que el número de tests unitarios.

Una vez que se ha probado que dos módulos, objetos o capas se integran bien, no es necesario repetir el test para otra variante de la lógica de negocio; para eso habrán varios tests unitarios. Aunque los tests de integración pueden saltarse las reglas, por motivos de productividad es conveniente que traten de ser inocuos y rápidos. Si tiene que acceder a base de datos, es conveniente que luego la deje como estaba. Por eso, algunos frameworks para Ruby y Python entre otros, tienen la capacidad de crear una base de datos temporal antes de ejecutar la batería de tests, que se destruye al terminar las pruebas.

Como se trata de una herramienta incorporada, también hay quien ejecuta los tests unitarios creando y destruyendo bases de datos temporales pero es una práctica que debe evitarse porque los segundos extra que se necesitan para eso nos hacen perder concentración. Los tests unitarios deben pertenecer a suites diferentes a los de integración para poderlos ejecutar por separado. En los próximos capítulos tendremos ocasión de ver tests de integración en detalle.

Concluimos el capítulo sin revisar otros tipos de tests, porque este no es un libro sobre cómo hacer pruebas de software exclusivamente sino sobre cómo construir software basándonos en ejemplos que ilustran los requerimientos del negocio sin ambigüedad. Los tests unitarios, de integración y de aceptación son los más importantes dentro del desarrollo dirigido por tests.


Copyright (c) 2010-2013 Carlos Ble. La copia y redistribución de esta página se permite bajo los términos de la licencia Creative Commons Atribución SinDerivadas 3.0 Unported siempre que se conserve esta nota de copyright.