lunes, 28 de junio de 2010

Como lograr tests de unidad rápidos

Una de las principales características deseables en los tests de unidad de un proyecto es que sean rápidos. El típico ciclo de TDD de Red-Green-Refactor funciona mucho mejor cuando los tests se pueden ejecutar muy frecuentemente porque cuanto más tiempo dejamos pasar entre corridas de tests más dificil se hace saber que puede haber causado la falla de un test.

Ahora bien, ¿Qué tan rápidos necesitamos que sean nuestros tests? Digamos que tenemos tests que tardan en promedio 1 segundo. No parece demasiado lento, pero si hay 500 tests (un número fácil de alcanzar en un proyecto mediano) entonces la ejecución de estos tests llevaria más de 8 minutos, lo que nos impediría correr nuestros tests mas de dos o tres veces por hora.

Normalmente los tests que retrasan las corridas son aquellos que usan recursos externos, en especial las bases de datos. Michael Feathers considera que estos no son realmente tests de unidad, pero esto no quita que en el acceso a la base de datos puede haber errores y que esta funcionalidad se tiene que testear.

Una posibilidad para que los tests de unidad que tocan la base de datos no afecten al resto es armar dos test suites separadas. La test suite que no usa la BD se ejecuta dentro del ciclo normal de TDD y la otra solo en ocasiones especiales (por ejemplo antes de hacer commit en el repositorio). Este esquema es fácil de implementar, pero tiene el problema de que algunos tests de unidad se ejecutan mucho menos frecuentemente.

Otra alternativa, si uno tiene la suerte de trabajar en Ruby, es ZenTest. Esta herramienta tiene muchas utilidades para unit testing, pero la que nos interesa ahora es autotest que detecta los cambios que realizamos en el código y ejecuta solo los tests afectados por dichos cambios. También hay una herramienta parecida para Java, aunque no he tenido oportunidad de probarla. Con este tipo de herramientas ya no nos preocupa tener tests lentos, ya que solo vamos a ejecutar unos pocos por vez.

Una tercera posibilidad es extraer la funcionalidad que accede a la base de datos y esconderla detras de una Facade que funciona como un repositorio. Los tests de unidad de este repositorio acceden a la base de datos, pero los del resto de la aplicación no, ya que utilizan una implementación alternativa del repositorio que guarda los datos en memoria. De esta manera solo los tests que verifican realmente el acceso a la BD pagan la penalidad del mayor tiempo de ejecución mientras que el resto corre utilizando solo memoria. Para complementar este esquema se puede tener otra suite que ejecute todos los tests con acceso real a la base de datos. Esta suite no debería normalmente detectar errores que no detecte la suite normal, pero se la puede ejecutar como reaseguro, por ejemplo en un servidor de integración continua.

Aplicando este esquema mejoramos además el diseño ya que rompemos la dependencia entre nuestra aplicación y la DB. Los objetos de negocio dejan de tener la responsabilidad de saber persistirse y esa responsabilidad pasa al repositorio. Esta situación donde buscando que nuestra aplicación sea más testeable mejoramos su diseño es muy común, porque en general mejoras en la testabilidad se traducen en disminución del acoplamiento.

Existen otras alternativas, más a nivel de optimización de recursos, como utilizar implementaciones de bases de datos en memoria (esto es particularmente fácil con SQLite) o correr los tests en paralelo, explotando que los tests de unidad deberían ser independientes unos de otros. Con esta última idea hay que tener mucho cuidado porque dos tests que utilicen la misma BD y se ejecuten al mismo tiempo pueden fallar en forma impredecible.

Como ven hay muchas posibilidades. Lo importante es maximizar el feedback de los tests de unidad para lo cual es indispensable poder ejecutar los tests muchas veces por hora.

No hay comentarios:

Publicar un comentario