La polémica: TDD está muerto

Recientemente David Heinemeier Hansson, creador del framework Rails, publicó un artículo titulado TDD is dead, long live testing que causó cierto debate con referentes de la disciplina. Incluso Uncle Bob y Kent Beck dedicaron incluso algunas líneas a la cuestión.

Personalmente creo que TDD es una práctica muy útil, la uso a menudo, pero no todo el tiempo ni para todo. Me gusta y suelo usar  TDD cuando tengo que escribir lógica negocio. Generalmente no uso TDD cuando tengo que escribir lógica de presentación y cosas por el estilo.

Lo que destaco de este intercambio de opiniones es que me ha dado material para trabajar con mis alumnos :-).

Clasificación de Pruebas

Existen distintas clasificaciones para las pruebas de software. Desde el punto de vista de cómo se ejecutan, podemos clasificar las pruebas en manuales o automatizadas.

 

Por otro lado, desde el punto de vista de qué es lo que prueban, yo suelo clasificarlas en primera instancia en unitarias o no-unitarias.

 

La prueba unitaria prueba un componente en concreto. Esto implica aislar al componente bajo prueba de sus dependencias. Pues si no lo aislamos y la prueba falla, no tendremos la certeza de si la falla se debió a un error en el componente bajo prueba o si se debió a un problema en una de sus dependencias. Es aquí, donde entran en juego los llamados Test Doubles que no ayudan a aislar el componente bajo prueba.

 

Por su parte, dentro de lo suelo llamar pruebas no-unitarias, entran todos los demás tipos de pruebas, las cuales implican probar la interacción entre distintos componentes. En este tipo de pruebas los componentes ya no están aislados y por ello algunas personas las llama pruebas de integración. En este grupo están tanto las pruebas de aceptación, como las de stress y cualquier otra prueba que implique la interacción en distintos componentes. Dado que hay mucho que decir sobre este tipo de pruebas, dedicaré otro post exclusivo.

La clave es educarlos de chiquitos

Hoy compartí el almuerzo con mi colega JuanG y algunos otros profesionales de la informática. En un momento comenzamos a hablar sobre TDD y algunas otras prácticas ágiles. Durante la charla uno de los comensales comentó que le parecía muy difícil pretender que los programadores «junior» utilicen dichas técnicas cuando las mismas ni se mencionan en las universidades. Estuve de acuerdo con el comentario respecto de la complejidad, pero al mismo tiempo agregué que en algunas universidades, estos temas son materia corriente. Para mi : «La clave es educarlos de chiquitos».

Como ocurre en todos los ámbitos de la vida, una vez que algo se hace hábito, cuesta mucho cambiarlo. Es por esto que en las materias que dicto hago mucho énfasis en cuestiones tales TDD y prueba unitaria. Un caso testigo es lo que hacemos en Algo3. En las clases prácticas resolvemos ejercicios y en todos los casos lo hacemos usando TDD. Luego, en los dos trabajos prácticos individuales que los almnos deben resolver, les recomendamos una y otra vez que los resuelvan utilizando TDD. Finalmente cuando llegan al último trabajo práctico que implica trabajar el grupo, los alumnos tienen la técnica incorporada. Un detalle importante es que si bien la técnica de TDD la aprenden en nuestra material, ya en la materia anterior los alumnos comienzan a hacer pruebas unitarias automatizadas.

Cierro este post con algunos gráficos de métricas de tests de los grupos que estoy dirigiendo en Algo3 este cuatrimestre. ¡Hermoso!

covertura

Probando excepciones

¿Como probar que un método lanza una excepción ante una determinada situación excepcional? Usando NUnit o JUnit 4, basta con escribir el método de prueba y poner una simple anotación indicando el tipo de excepción esperada.

@Test(expected = ExceptionEsperada.class)  
public void xxxxxx(){
      // ejecutamos la código que debiera lanzar la excepción
}

Pero no todos los xUnit brindan esta posibilidad, entonces debemos apelar a una estrategia similar a la siguiente.

public void test_xxxxxx(){
     try{
         // ejecutamos la código que debiera lanzar la excepción
         fail(); // si ejecución llega hasta esta línea, entonces significa que no se lanzó la excepción esperada y por ende la prueba ha fallado
      }
      catch(ExceptionEsperada ex){
          /* si estamos aqui, entonces se ha producido la excepción esperada y no es necesario
             hacer nada pues a menos que se indique que algo ha ido mail, se asume todo estuvo 
             bien */
       }
       catch(Exception ex){
          // si estamos aqui, es que la excepción que se ha lanzado, no es la esperada, por lo tanto la prueba ha fallado
          fail();
       }
    }

Asi de simple, espero les resulte útil.

Escribiendo pruebas unitarias para código legacy

Desde que volví a trabajar en consultoría este es uno de los temas que más me he encontrado. Sinceramente no me sorprende pues:

  • TDD y la automatización de pruebas, son dos temas que están en claro ascenso de popularidad
  • Casi toda la bibliografía de TDD parte de la base de la creación de aplicaciones desde cero
  • Pero en una porción importante de casos, la gente ya cuenta con una aplicación, la cual muchas veces no ha sido diseñada de forma de facilitar su prueba

Casualmente ayer me crucé con un caso extremo. Resulta que hace un par de semanas dicté un workshop de TDD. Ayer me contactó uno de los asistentes para hacer una consulta de como encarar un caso concreto: tenia que agregar una funcionalidad a la una clase existente. La clase en cuestión tenia más de 3000 líneas de código. Si si, leiste bien, son tres ceros después del tres, o sea, tres mil líneas de código. El problema de una clase tan grande, es que resulta dificil que sea cohesiva. Se supone que pasar ser cohesiva, una clase debe hacer UNA cosa y hacer bien. Con tantas líneas, es muy posible que dicha clase esté haciendo demasiadas cosas.

Pero el problema que motivaba la consulta no eran las 3000 líneas de código, sino el constructor de dicha clase. Resulta que el constructor además de recibir varios parámetros, instanciaba un componente para conectarse a la base de datos. Eso impedia realizar una prueba unitaria de la clase, pues el solo hecho de instancialar implicaba conectarse a la base. Tampoco no habia chances de mockear la conexión pues era instanciada directamente dentro del constructor. ¿que hacer entonces?

Lo primero que uno intentaria es hacer un refactoring de la clase, pero dado que no existian pruebas de la misma, cualquier modificación implicaba un gran riesgo. Luego de analizar varias alternativas llegamos a una solución de compromiso, que nos permitiria escribir tests unitarios: agregar un constructor sin parámetros, que tenga la lógica para lanzar un excepción si era invocado en un ambiente distinto al de desarrollo/test. De esta forma podriamos instancias la clase en forma aislada en un ambiente de test y  aseguramos que no sea instanciada de esta forma en caso de estar en un ambiente distinto.