Tests de contrato
Prosiguiendo con mi inmersión en el mundo del TDD, hace poco he finalizado un curso online que me ha parecido excepcionalmente bueno. Impartido por JB Rainsberger, y con el modesto título de “World’s best intro to TDD”, durante más de 20 horas de vídeo, nos desgrana diferentes acercamientos (approaches que llaman) al desarrollo de software utilizando TDD.
Mi intención en este blog es ir hablando de estos acercamientos en el futuro, además de otras cosas que he aprendido en el curso. En cualquier caso, nada como realizarlo si estáis interesados en el tema (su precio, 97 dólares, me parece barato después de haberlo hecho, creedme). Comenzaré con una técnica que me ha gustado especialmente a la hora de trabajar con interfaces, conocida como “Tests de contrato” (o contract tests).
Qué es un contrato. Sintáxis y semántica.
Como todos sabemos, si programamos con interfaces crearemos mejores diseños, más encapsulados y cohesionados, y menos acoplados. Si no sabéis de lo que hablo, ya tardáis.
Un contrato no es más que una interfaz (y viceversa :)). El contrato contiene una sintáxis y una semántica. Veamos un ejemplo:
En el ejemplo hemos creado una calculadora que evalúa expresiones en cadenas de caracteres. La sintáxis del contrato nos dice que tenemos un método getSumFor
que recibe como entrada una expresión String
y devuelve un número de tipo long
.
No tenemos más información, por tanto, sin la documentación adecuada, ¿sabríamos utilizar esta interfaz sin ver la implementación? En una de las modalidades de TDD se alienta el uso de interfaces mediante el uso de Mocking frameworks (por ejemplo, Mockito) en nuestros tests. Es decir, cuando creamos la especificación de una clase con colaboradores, deberemos añadir dichos colaboradores como interfaces al test, y “mockear” su comportamiento. Veamos un ejemplo, de una supuesta clase controladora que utilizaría nuestra calculadora para evaluar expresiones introducidas por el usuario:
En el ejemplo estamos testeando un MVC, donde la vista es la clase Display
, y el modelo es la clase Calculator
. Creo que el código es bastante autoexplicativo, espero que quede claro el hecho de que tanto Display
como Calculator
son interfaces, mientras que Controller
es el “subject under test”.
Bien, la línea when(calculator.getSumFor("1,2")).thenReturn(3L)
nos está desvelando parte de la semántica de la clase Calculator
. Ya sabemos en parte que las expresiones son una lista de números separados por comas, y que el método getSumFor
devolverá un número conteniendo la suma de todos esos números.
El problema es que en la clase Controller
estamos testeando más bien las interaciones entre componentes, y no la semántica de Display
, que seguramente contenga muchos casos límite y de error. Aquí es donde surge el concepto que da título a este post.
Tests de Contrato
Un test de contrato es una conjunto de especificaciones de la semántica de una interfaz / contrato. Esta especificación se implementará en una clase abstracta de tests, que contendrá uno o más métodos abstractos de tipo factory, encargados de crear las implementaciones finales que deberán cumplir nuestro contrato. Esto se entenderá mucho mejor con el ejemplo:
Aquí tenemos el test de contrato para la clase Calculator
. A fines didácticos asumo que la expresión no será nula, no contendrá caracteres diferentes a dígitos, etc. Para que quede bien claro, en la práctica esta clase contendría muchos más métodos especificando múltiples casos de error.
En el ejemplo vemos como hay un método factory getCalculatorImpl
que se encarga de devolver el “subject under test”, pero al ser abstracto, ¡hará imposible ejecutar nuestros tests! Por este motivo este no es un test al uso, sino una forma de dejar claro qué espera nuestro sistema de las clases que implementen Calculator
.
Vamos, por tanto, a crear una implementación de nuestro contrato:
¿Diríais que esta implementación es correcta, basada en las especificaciones? Para ello lo que tendremos que hacer es crear una clase de test que extienda CalculatorContract
e implemente el método getCalculatorImpl
:
Esta clase ya no es abstracta, y por tanto podremos ejecutar los tests, que, efectivamente, son existosos.
Añadiendo nuevas implementaciones del contrato
Java 8 nos facilita mucho la vida para trabajar con listas y colecciones, gracias a las Lambda expressions y Streams. Hagamos uso de ello en una nueva implementación:
Espero que mentalmente ya hayáis anticipado el siguiente paso, que en efecto será crear una nueva implementación de CalculatorContract
:
Con este último ejemplo finalizamos el post. El ejemplo utilizado ha sido extremadamente sencillo, pero en la práctica nuestros métodos factory es posible que requieran parámetros para instanciar el “subject under test”, o incluso que necesitemos varios métodos factory simulando condiciones diferentes (fixtures). Espero, al menos, que la idea haya quedado bien clara.