Introducción a TDD

Exordio para puristas

Oh Madre Lengua Castellana, en nombre de la mano perdida del manco de Lepanto, perdónanos por las aberraciones que hemos de cometer en desmedro de tu ilustre prosapia bajo el estandarte de la jerga informática. Aparta tu justamente indignada mirada de palabrejas como "testear", "castear", "comitear" y engendros afines, rayanos en lo herético. Concédenos, al menos en esta ocasión, que lo importante es transmitir las ideas; un Rastrojero es tan bueno como un Lamborghini, en tanto lleve a sus pasajeros a destino de forma eficiente y eficaz.

Sin más, con la conciencia impoluta, pasemos a lo que nos compete:

¿Por qué testear una aplicación? Porque es la principal forma de encontrar defectos en el software. El testing no nos garantiza que el sistema testeado no tenga errores, pero nos permite encontrarlos. El testing nos permite elaborar un producto de mayor calidad.

Un buen testing nos da confianza para modificar la aplicación. Si el testing es completo, es posible modificar la aplicación con confianza, ya que si los cambios introducen un defecto, hay buenas chances de que sea detectado por el testing.

1 - Tipos de test (una clasificación posible)

  • Test unitario:

    Se prueba un bloque de código aislado de su entorno, vale decir su proyecto, solución, paquete, etc. Estos tests son automáticos, en el sentido de que no requieren entrada del usuario, y están escritos en el mismo lenguaje que el código a testear. En su forma más purista, un test unitario no debe acceder a ninguna dependencia externa como el sistema de archivos, bases de datos, servicios web externos, hardware especial, etc. Los motivos principales de estas restricciones son que los tests estén verdaderamente aislados, y además que corran lo más rápido posible. Tener tests unitarios veloces mantiene el ciclo de TDD productivo; si se llenan los tests de operaciones de entrada/salida, se volverán rápidamente un cuello de botella cuando crezcan en número. Uno de los propósitos esenciales de este tipo de tests es aislar los errores para facilitar su detección y eliminación. Ergo, este es el tipo de test donde TDD pone más énfasis.

  • Test de integración:

    Se prueba un conjunto de bloques de código en su entorno. Se supone que los mismos fueron testeados individualmente; idealmente, con tests unitarios. Este tipo de test permite probar la interacción de los bloques en cuestión y tener la garantía de que funcionan bien en conjunto.

  • Test de regresión:

    Se prueba que las funcionalidades ya verificadas en versiones anteriores sigan funcionando en una nueva versión.

  • Test de estrés:

    Se analiza la respuesta de la aplicación en condiciones de funcionamiento críticas: miles de archivos de entrada, requerimientos de tiempo real estricto, etc. Dichas condiciones dependerán de la aplicación en particular.

  • Test funcional:

    Se prueba que la aplicación satisfaga los requerimientos.

  • Test de aceptación:

    Caso especial de test funcional en el que se hace una lista de pruebas manuales sobre la aplicación, típicamente en presencia del cliente. Una vez que pasen los tests y el cliente de el visto bueno, se da por entregada y concluida la funcionalidad probada.

2 - El test como realización de un caso de uso

Este concepto es la piedra angular de TDD, y dado que es totalmente opuesto al paradigma clásico de enseñanza de la programación que implica implementar primero y probar después, es un poco más difícil de asimilar para quienes fueron educados en dicho paradigma.

TDD concibe el test como una forma de escribir un requerimiento en un lenguaje libre de ambigüedades: el código fuente. Se utiliza el test como una especificación del funcionamiento del componente a desarrollar y como forma de verificar la funcionalidad. Siendo el test una especificación, no debería ser necesario escribir más que el código mínimo para que los tests funcionen; escribir más que eso estaría introduciendo código sin testear, algo que se desea evitar en la etapa inicial de TDD.

Por lo tanto, para escribir los tests es necesario partir de una especificación de requerimientos, típicamente los casos de uso o user stories, aunque el formato puede ser otro. Un enfoque posible es armar una clase de test por cada caso de uso, lo cual es efectivo en tanto los casos de uso sean de una complejidad comparable. Tal vez haya que descomponer algún caso muy complejo, idealmente en subcasos independientes. En todo caso, es importante que el código de los métodos de cada clase de tests sea bien sencillo, corto y autodocumentado. El secreto para lograr esto es escribir los tests antes de implementar la funcionalidad.

Una vez escritos los tests, la única preocupación de los desarrolladores será que pasen. Desde este momento, entran en el ciclo de TDD:

Sobre este diagrama, es necesario aclarar que para pasar de 1 a 2, inicialmente se hace el mínimo esfuerzo, y sin parar mientes en las buenas prácticas. Eso queda pospuesto para la etapa de refactoring. En otras palabras, temporalmente es aceptable devolver constantes, copiar y pegar código ciegamente, hacer un switch por tipo, lo que sea que conduzca inmediatamente a que pasen los tests. Este es otro aspecto de TDD que puede resultar chocante a los neófitos. La razón para tomar estos atajos es llegar a tener una suite de tests que pasen lo antes posible, ya que cubrirá futuros cambios. Ya desde el primer momento, se tiene la seguridad de que la funcionalidad está cubierta y protegida. Y esto se mantendrá incluso en ciclos posteriores donde el código ya tenga una calidad aceptable.

Continuará...

Versión en inglés / English version

Comments

Popular posts from this blog

VB.NET: Raise base class events from a derived class

Apache Kafka - I - High level architecture and concepts

Upgrading Lodash from 3.x to 4.x