domingo, 17 de enero de 2010

How To: Contract Tests (Pruebas de Contrato)


El diseño de un API es un problema bastante complejo. Hay que tener multitud de cosas en mente (modularidad, escalabilidad, extensibilidad, usabilidad, simpleza, etc). Si os interesa el tema os recomiendo este libro de Jaroslav Tulach (uno de los arquitectos de NetBeans). Es un libro difícil, como el problema que aborda, pero muy bueno.
En dicho libro conocí el concepto de Contract Tests(Pruebas de Contrato). Sin embargo, aunque me pareció una buena idea, no le dí mucha importancia en aquel momento (hace un año, aproximadamente). Antes de fin de año, J.B. Rainsberger twitteaba el vídeo de Ben Rady escribiendo Contract Tests en Junit 4 y todo el tema volvió a mi cabeza.

¿Qué son las Pruebas de Contrato?
J.B. Rainsberger lo explica en este artículo (Que escribió en el 2005, menudo crack). Básicamente, las Pruebas de Contrato son una batería de pruebas que especifican el comportamiento de un determinado interfaz (o clase abstracta). Cualquier implementación de dicho interfaz (o cualquier clase derivada de la clase abstracta) debe superar dicha batería de pruebas para ser considerada correcta.

¿Qué problema resuelven las Pruebas de Contrato?
Normalmente, cuando se crea un interfaz (o una clase abstracta) es para que tenga varias implementaciones (o clases derivadas). Sucede lo mismo con un API, puede tener más de una implementación pero un único interfaz.
Si no se define un comportamiento general, es posible que las implementaciones no sean intercambiables entre sí, violando así el principio de sustitucion de Liskov y, lo que es más importante, dejando a los clientes de dicho interfaz con el culo al aire. Dicho comportamiento es lo que se define como contrato. Podemos escribir dicho contrato como un documento más, con los problemas que ello conlleva (Código y documentación desincronizada, interpretaciones subjetivas de lo escrito, etc) o podemos escribir dicho contrato mediante pruebas.

¿Por qué me interesan las Pruebas de Contrato?
Imagino que estaréis pensando:

Vaya chapa nos está metiendo el Peña.

Así que voy a contaros un poco mi motivación. Mi pensamiento tras ver el vídeo de Ben Rady fue:

¡Coño! Que bueno. Si lo hubiera aplicado antes a mi proyecto ahora sería mucho más feliz.

El proyecto actual de mi equipo consiste en diseñar (e implementar, testear, etc. Nada de waterfallismo :D )un API y crear varias implementaciones de dicho API (y diseñarlas, testearlas, etc. :D ).
Cuando comenzó el proyecto no le dimos importancia a esto de las Pruebas de Contrato (Sobre todo por desconocimiento). Tampoco pasaba nada, solo existía una implementación para la cual teníamos una batería de pruebas.
Más adelante añadimos una segunda implementación y, en lugar de convertir los test de la anterior implementación en Pruebas de Contrato, hicimos un corta-pega del demonio (Me da vergüenza escribir esto, pero de los errores se aprende).
No os recomiendo este enfoque :D Las pruebas huelen a DRY que tiran de espaldas.
Además, aceptamos pequeños cambios en el comportamiento de las implementaciones ya que, al definir el contrato en un wiki en lugar de hacerlo con pruebas, malinterpretamos ciertos detalles (algunas veces a propósito :S ).
Le he planteado al equipo que, las nuevas pruebas funcionales que creemos sean Pruebas de Contrato, a ver si conseguimos eliminar duplicaciones.
NOTA: Tengo que aclarar que estoy muy contento con la marcha del proyecto :D Lo que pasa es que mi nivel de exigencia aumenta cada mes. De hecho, nuestro equipo es famoso por lo mal que habla de su propio código, estando dicho código bastante por encima de la media. Somos un equipo muy autoexigente y muy autocrítico.

Un caso práctico: El interfaz Collection con JUnit 4
Después de todo este rollo, vamos a la chicha.

Vamos a hacer unas Pruebas de Contrato para el interfaz Collection (No vamos a hacer el contrato entero porque nos puede dar un chungo).
Normalmente, las Pruebas de Contrato se añaden al proyecto que contiene los intefaces a definir. Las clases que ejecutan las pruebas para cada implementación se añaden al proyecto que contiene dicha implementacion. Como yo no tengo acceso a dichos proyectos, me he creado uno propio. He metido todas las clases en ese proyecto, pero he hecho una separación en paquetes para que entendáis un poco la distribución de las clases. Podéis verlo aquí (Es de NetBeans. A mi me gusta mucho, pero Xavi Gost me diría que madurara... Aún así, es tan sencillito que no creo que de problemas :D ).

Lo primero es crear una clase abstractra que contenga todos los test de contrato y un método abstracto que devuelva un objeto Collection. Yo he hecho la siguiente:



Como podéis ver, el método nuevaColeccion se llama en el setUp para no tener que escribirlo en cada prueba. Además, dicho setUp es final para que no pueda ser sobreescrito por las clases hijas.

Si ahora lanzamos las pruebas obtenemos el siguiente resultado:



Si os fijáis, la clase abstracta que hemos creado no termina en Test, así que JUnit 4 no la tiene en cuenta a la hora de ejecutar las pruebas.

Ahora vamos a probar las implementaciones. Empezamos por ArrayList, para lo que añadimos la siguiente clase:



Lo mismo para HashSet:



Si ahora pasamos las pruebas obtenemos:



¡Bien! Pasan todas las pruebas :D Eso quiere decir que ambas implementaciones cumplen con el contrato y todos somos un poco más felices.
Hay que destacar que las pruebas se están contabilizando en cada clase de prueba de cada implementación (Todas las Pruebas de Contrato se ejecutan en ArrayListTest y en HashSetTest). A la hora de contabilizar pruebas la clase base no existe :D

Vamos a ponérselo un poco más difícil a las implementaciones. Añadimos la siguiente prueba a la clase que define el contrato:



Si ahora pasamos las pruebas de las implementaciones obtenemos:



¿Rojo? mmmmm Huele a violación de los principios S.O.L.I.D.
La implementación HashSet no supera la nueva prueba que hemos añadido. Hemos definido un contrato demasiado estricto y algunas implementaciones no lo soportan.
En este caso, no podemos suponer lo que hará una colección cuando se le añada el mismo objeto varias veces. Depende de la clase derivada. Claramente, las implementaciones de Collection violan el principio de Liskov :D

Podríamos seguir añadiendo pruebas y tal, pero imagino que ya ha quedado más o menos claro ¿No? Ya veis que no sería muy complicado organizar las pruebas que ya tenemos para obtener unas cuantas Pruebas por Contrato. Espero poder hacerlo en mi equipo :D (Me temo que esto es deuda técnica).

Conclusiones
  • Las Pruebas de Contrato son una especificación del API.
  • Las Pruebas de Contrato se pueden usar como documentación del interfaz. La documentación escrita en prosa "a la antigua usanza" es mucho más fácil de malinterpretar.
  • El contrato puede ser todo lo estricto que queramos. Hay que aplicar el sentido común para saber cuando parar.
  • Mediante Pruebas de Contrato eliminamos duplicidad de código de test. Las pruebas hay que seguir escribiéndolas tengamos o no Pruebas de Contrato y, escribir las mismas (o parecidas) en cada una de las implementaciones es una perdida de tiempo y un infierno a la hora de mantenerlas (lo digo por experiencia).
  • Definir unas Pruebas por Contrato puede ayudarnos a descubrir problemas de diseño en el API. Una violación del principio de Liskov es un problema en el API

Esto es todo amigos. Espero que os haya gustado mi primera entrada técnica :D

Foto de "portada": Galería de Mark, bajo licencia Creative Commons

No hay comentarios:

Publicar un comentario