There is a discussion in the spanish TDD group about whether it is good or bad, when you have a stub object, to return a different value depending on the input arguments of the method. I don't think it's the right thing to do because, normally, when a stub object cares about the input arguments in its method calls it's because it should not be a stub object, it should be a mock object. Let me explain it with an example:
The payment gateway is a stub that returns true when user_has_funds is called with an input argument of 50.
If we look carefully at the test, what we are really doing is testing more than one behavior. First, we're asserting that a user can buy stuff if she has sufficient funds. Second, we are expecting that the payment gateway is called with the right amount.
So, if it is an expectation, why don't we use a mock instead of a stub? We're mixing the concepts of mock and stub and, as a consequence, we're testing more than one thing.We're breaking the single responsibility principle.
Steve Freeman and Nat Pryce, authors of the great Growing objects oriented software, guided by tests, recommend to follow a simple rule of thumb, "specify exactly what you want to happen and no more". If we listen to them (and we should!), those are the resulting tests:
The first test specifies the relationship between the order and the payment gateway (The message protocol). It uses a mock object to replace the gateway payment object and it expects a call at user_has_funds method with the order amount as an input argument. We want to know that the payment gateway is going to be called with the right amount, so we are interested in its input argument (which is right when mocking). The second one specifies the behavior of the order when the user has funds, and I don't really care about how the order interacts with the payment gateway. I just want to know that the user has sufficient funds. That's why, in this case, we use a stub.
What I like about the new tests is that, if we change the order behavior by adding some taxes to the product price, the second test is not going to break (we still confirm the order if the payment gateway says that the user has funds, nothing has changed in that specification). Only the first one would be red (and for the right cause, we've changed exactly that specification).
What do you think?
PS: I know that, in RSpec:mocks, stub() is an alias for mock() and I don't like it very much :P
Mostrando entradas con la etiqueta SOLID. Mostrar todas las entradas
Mostrando entradas con la etiqueta SOLID. Mostrar todas las entradas
miércoles, 9 de febrero de 2011
miércoles, 24 de noviembre de 2010
Más sobre S.O.L.I.D.
En el último post os intenté convencer de que habíamos conseguido no violar el principio de inversión de dependencias con nuestra clase Banco. Si no lo recordáis, nos quedó algo como esto:
José Luis Barrera me sugirió en los comentarios que utilizáramos una clase Credenciales que agrupara tanto el nombre del usuario como el Pin. Con esa nueva abstracción, el código de Banco queda aún más claro:
Guay :D
Ahora bien, ¿qué ocurre cuando la validación de las Credenciales falla? Tal y como tenemos ahora mismo nuestra implementación, OperacionesBancarias lanzaría una excepción por cada tipo de error que se produzca al validar el usuario. Por ejemplo, si lo que falla es que las Credenciales son incorrectas se produce una excepción CredencialesIncorrectas. Si lo que sucede es que la validación de las Credenciales ha sido fallida más de tres veces, la excepción que se lanza es una AccesoInvalidadoPorMultiplesReintentosFallidos. Si lo que se produce es un error en el acceso al banco real, se lanzará un BancoInaccesible.
Veamos como implementaríamos un cliente de Banco (un Cajero) que se preocupe del resultado de la validación:
Pues no parece que sea muy legible... No se vosotros, pero yo estoy bastante acostumbrado a este tipo de código y tengo que decir ¡basta ya!
Las excepciones que captura Cajero no son parte de la abstracción de Banco, son detalles de bajo nivel que se encuentran en OperacionesBancariasBancoManolito, con lo que estamos volviendo a violar el principio de inversión de dependencias. Además, estamos controlando el flujo del programa mediante excepciones. ¡Que desastre!
Las excepciones deberían ser para casos excepcionales
Como bien nos contó (recordó) Enrique Comba en el curso de T.D.D., las excepciones deben ser excepcionales. No tiene sentido usarlas para controlar el flujo porque, en ese caso, dejan de ser excepcionales. Esto que parece tan trivial es una de las cosas que más nos cuesta cuando nos ponemos a programar.
Con esto en mente, volvamos a nuestro ejemplo, ¿Es excepcional que unas Credenciales sean incorrectas? ¿Es excepcional que un Banco tenga que anular una tarjeta porque el usuario se ha equivocado n veces al intentar usarla? De nuestro ejemplo, el único caso "excepcional" puede ser que el banco real no esté accesible pero ¿de verdad es tan raro que haya un corte de comunicaciones? Para mi, ninguno de estos casos es merecedor de una excepción, así que vamos a intentar arreglarlo. Lo primero que vamos a hacer es que Cajero no dependa de los detalles de OperacionesBancariasBancoManolito (y si de paso evitamos controlar el flujo con excepciones, mejor que mejor). Para ello, vamos a pasarle al Banco la responsabilidad de avisar al Cajero cuando suceda un evento de validación (correcta, incorrecta, invalida, sin conexión). Refatorizamos nuestro Banco para que quede como sigue:
y nuestro Cajero ahora queda mucho más limpio:
Aunque nuestra clase Cajero ha quedado mucho más clara, hemos pasado el problema de las excepciones a la clase Banco. Además, estamos violando el principio de segregación de interfaces (I). Vayamos paso a paso.
Segregación de interfaces
Para cumplir este principio, nuestra clase Cajero debe implementar un interfaz para manejar los eventos de validación, que es lo único que la clase Banco necesita conocer de Cajero:
Y nuestra clase Banco dejaría de depender de Cajero para depender de dicho interfaz:
Ahora que nuestra clase Cajero ya cumple el principio de segregación de interfaces, arreglemos Banco.
La solución que se me ha ocurrido es que sea la abstracción Token la que nos de la información que actualmente nos dan las excepciones:
Ahora el flujo ya no es guiado por excepciones pero, lo que me parece más importante aún, es que nuestro código ahora es más sencillo de extender. Antes necesitábamos ir a un javadoc (o similar) para leer que tipo de excepciones eran necesarias para que Banco funcionara, ahora basta con implementar Token.
Por último, tengo que decir que no me gustan las estructuras if-elseif-elseif-else, pero para este caso en particular no me parece tan horrible. Si los motivos por los que Token puede no ser válido fueran más o pudieran cambiar más a menudo me pensaría una solución con suscriptores similar a la que hemos utilizado con el Cajero. Si os apetece hacerlo os lo dejo como ejercicio :P
Perdonad que me haya quedado una entrada tan larga y atolondrada. Espero que al menos se entienda lo que he intentado expresar :D ¡Nos vemos en la siguiente!
Nota: No haría estas refactorizaciones que he hecho aquí si no tuviera una buena base de pruebas contra las que probar cada pasito.
José Luis Barrera me sugirió en los comentarios que utilizáramos una clase Credenciales que agrupara tanto el nombre del usuario como el Pin. Con esa nueva abstracción, el código de Banco queda aún más claro:
Guay :D
Ahora bien, ¿qué ocurre cuando la validación de las Credenciales falla? Tal y como tenemos ahora mismo nuestra implementación, OperacionesBancarias lanzaría una excepción por cada tipo de error que se produzca al validar el usuario. Por ejemplo, si lo que falla es que las Credenciales son incorrectas se produce una excepción CredencialesIncorrectas. Si lo que sucede es que la validación de las Credenciales ha sido fallida más de tres veces, la excepción que se lanza es una AccesoInvalidadoPorMultiplesReintentosFallidos. Si lo que se produce es un error en el acceso al banco real, se lanzará un BancoInaccesible.
Veamos como implementaríamos un cliente de Banco (un Cajero) que se preocupe del resultado de la validación:
Pues no parece que sea muy legible... No se vosotros, pero yo estoy bastante acostumbrado a este tipo de código y tengo que decir ¡basta ya!
Las excepciones que captura Cajero no son parte de la abstracción de Banco, son detalles de bajo nivel que se encuentran en OperacionesBancariasBancoManolito, con lo que estamos volviendo a violar el principio de inversión de dependencias. Además, estamos controlando el flujo del programa mediante excepciones. ¡Que desastre!
Las excepciones deberían ser para casos excepcionales
Como bien nos contó (recordó) Enrique Comba en el curso de T.D.D., las excepciones deben ser excepcionales. No tiene sentido usarlas para controlar el flujo porque, en ese caso, dejan de ser excepcionales. Esto que parece tan trivial es una de las cosas que más nos cuesta cuando nos ponemos a programar.
Con esto en mente, volvamos a nuestro ejemplo, ¿Es excepcional que unas Credenciales sean incorrectas? ¿Es excepcional que un Banco tenga que anular una tarjeta porque el usuario se ha equivocado n veces al intentar usarla? De nuestro ejemplo, el único caso "excepcional" puede ser que el banco real no esté accesible pero ¿de verdad es tan raro que haya un corte de comunicaciones? Para mi, ninguno de estos casos es merecedor de una excepción, así que vamos a intentar arreglarlo. Lo primero que vamos a hacer es que Cajero no dependa de los detalles de OperacionesBancariasBancoManolito (y si de paso evitamos controlar el flujo con excepciones, mejor que mejor). Para ello, vamos a pasarle al Banco la responsabilidad de avisar al Cajero cuando suceda un evento de validación (correcta, incorrecta, invalida, sin conexión). Refatorizamos nuestro Banco para que quede como sigue:
y nuestro Cajero ahora queda mucho más limpio:
Aunque nuestra clase Cajero ha quedado mucho más clara, hemos pasado el problema de las excepciones a la clase Banco. Además, estamos violando el principio de segregación de interfaces (I). Vayamos paso a paso.
Segregación de interfaces
Clients should not be forced to depend upon interfaces that they do not use.
Los clientes no deben verse forzados a depender de interfaces que no usan.
Para cumplir este principio, nuestra clase Cajero debe implementar un interfaz para manejar los eventos de validación, que es lo único que la clase Banco necesita conocer de Cajero:
Y nuestra clase Banco dejaría de depender de Cajero para depender de dicho interfaz:
Ahora que nuestra clase Cajero ya cumple el principio de segregación de interfaces, arreglemos Banco.
La solución que se me ha ocurrido es que sea la abstracción Token la que nos de la información que actualmente nos dan las excepciones:
Ahora el flujo ya no es guiado por excepciones pero, lo que me parece más importante aún, es que nuestro código ahora es más sencillo de extender. Antes necesitábamos ir a un javadoc (o similar) para leer que tipo de excepciones eran necesarias para que Banco funcionara, ahora basta con implementar Token.
Por último, tengo que decir que no me gustan las estructuras if-elseif-elseif-else, pero para este caso en particular no me parece tan horrible. Si los motivos por los que Token puede no ser válido fueran más o pudieran cambiar más a menudo me pensaría una solución con suscriptores similar a la que hemos utilizado con el Cajero. Si os apetece hacerlo os lo dejo como ejercicio :P
Perdonad que me haya quedado una entrada tan larga y atolondrada. Espero que al menos se entienda lo que he intentado expresar :D ¡Nos vemos en la siguiente!
Nota: No haría estas refactorizaciones que he hecho aquí si no tuviera una buena base de pruebas contra las que probar cada pasito.
Etiquetas:
dirigidoportests,
pair programming,
SOLID,
TDD
jueves, 18 de noviembre de 2010
Violando la D de S.O.L.I.D
A principios de semana tuve la suerte de asistir al curso de TDD que impartió Enrique Comba en Madrid. Me lo pasé genial pero me fui aún más convencido de que programar es muy difícil.
Aunque el temario del curso fue bastante amplio, yo sólo voy a centrarme en los principios S.O.L.I.D. Si queréis saber más sobre lo que hicimos allí, Jesús Jiménez ha escrito este post explicándolo.
El primer día del curso, Enrique nos dividió en 5 grupos y nos asignó la exposición de un principio S.O.L.I.D. a cada equipo. A nosotros (Amalia Hernandez, Jesús Jiménez, Leo Antolí y yo) nos tocó explicar la D.
Inversión de Dependencias (D)
Cuéntamelo con código
El segundo día lo dedicamos a crear el software que controla un cajero automático (haciendo T.D.D., claro). El código que creó nuestro equipo (los mismos cuatro que el día anterior) lo tenéis completo aquí, pero yo me voy a centrar sólo en nuestra implementación de la clase Banco:
Lo que hace la clase Banco es realizar una petición de validación mediante el Conector a una url. Dicha validación nos devuelve un json a partir del cual se puede crear el token de seguridad con el que se realizarán las siguientes operaciones del usuario validado.
¿Habremos sido capaces de respetar el principio que nos tocó explicar el día anterior?
Los módulos de alto nivel (Banco) no deben depender de módulos de bajo nivel(Implementaciones de Conector y GeneradorToken), ambos deben depender de abstracciones. Nuestro Banco depende de la abstracción Conector y de la abstracción GeneradorToken, pero no "conoce" que implementación de cada abstracción está usando (Ambas se le inyectan en el constructor). Parece que esta parte es correcta.
Las abstracciones (Banco) no deben depender de detalles. Los detalles deben depender de abstracciones. En esta parte es donde hemos metido la pata. Si Banco no debe depender de detalles ¿Qué pinta la construcción de la url contra la que debe operar el Conector? ¿Por qué el Banco conoce que el Conector devuelve json?
Pensando en esto se me ocurre el siguiente refactor de Banco:
Nuestro módulo de alto nivel depende ahora de una abstracción más general, dejándole a los módulos de bajo nivel (Las implementaciones de OperacionesBancarias) todo lo que tiene que ver con la infraestructura (tipo de comunicación, transformación de la respuesta, etc). Pero, ¿no es el token un detalle de bajo nivel? Si la respuesta es afirmativa deberíamos eliminarlo de Banco y OperacionesBancarias devolvería directamente la Cuenta, haciendo que nuestra clase Banco fuera redundante. Sin embargo, yo (que soy el que está programando :P ) creo que cualquier autenticación bancaria me va a devolver un token (hablo desde la ignorancia, pero suena bien) con lo que deja de ser un detalle para formar parte de la abstracción. Eso sí, no estaría mal que fuera una clase Token en lugar de un String, que no todos los tokens tienen porque ser iguales. La clase Banco que no viola el principio de inversión de dependencias quedaría así:
Conclusiones
Yo ya conocía los principios S.O.L.I.D. y se que mis compañeros también (aunque de este código tenemos la culpa Leo y yo :D ). Nos habíamos preocupado de leerlos y de intentar entenderlos mucho antes de dar este curso. Entonces, ¿por qué no fuimos capaces de recordar la dichosa D. incluso habiendo tenido que explicarla el día anterior? Yo pienso que es porque no lo tenemos interiorizado. Hace falta mucha práctica y mucha experiencia trabajando con los principios S.O.L.I.D. en la cabeza para que no se te olviden mientras programas. Por eso considero importante practicar T.D.D. con ejemplos sencillos, porque lo importante no es resolver el problema, lo importante es el proceso mental con el que resuelves el problema.
Criticadme, por favor
Lo que os he contado en este artículo es como entiendo yo el principio de inversión de dependencias. ¿Coincide con lo que entendéis vosotros? Si no es así, ¿en que me he equivocado?
Aunque el temario del curso fue bastante amplio, yo sólo voy a centrarme en los principios S.O.L.I.D. Si queréis saber más sobre lo que hicimos allí, Jesús Jiménez ha escrito este post explicándolo.
El primer día del curso, Enrique nos dividió en 5 grupos y nos asignó la exposición de un principio S.O.L.I.D. a cada equipo. A nosotros (Amalia Hernandez, Jesús Jiménez, Leo Antolí y yo) nos tocó explicar la D.
Inversión de Dependencias (D)
High level modules should not depend upon low level modules. Both should depend upon abstractions
Abstractions should not depend upon details. Details should depend upon abstractions
Los módulos de alto nivel no deberían depender de módulos de bajo nivel. Ambos deberían depender de abstracciones
Las abstraccciones no deberían depender de los detalles. Los detalles deberían depender de las abstracciones
Cuéntamelo con código
El segundo día lo dedicamos a crear el software que controla un cajero automático (haciendo T.D.D., claro). El código que creó nuestro equipo (los mismos cuatro que el día anterior) lo tenéis completo aquí, pero yo me voy a centrar sólo en nuestra implementación de la clase Banco:
Lo que hace la clase Banco es realizar una petición de validación mediante el Conector a una url. Dicha validación nos devuelve un json a partir del cual se puede crear el token de seguridad con el que se realizarán las siguientes operaciones del usuario validado.
¿Habremos sido capaces de respetar el principio que nos tocó explicar el día anterior?
Los módulos de alto nivel (Banco) no deben depender de módulos de bajo nivel(Implementaciones de Conector y GeneradorToken), ambos deben depender de abstracciones. Nuestro Banco depende de la abstracción Conector y de la abstracción GeneradorToken, pero no "conoce" que implementación de cada abstracción está usando (Ambas se le inyectan en el constructor). Parece que esta parte es correcta.
Las abstracciones (Banco) no deben depender de detalles. Los detalles deben depender de abstracciones. En esta parte es donde hemos metido la pata. Si Banco no debe depender de detalles ¿Qué pinta la construcción de la url contra la que debe operar el Conector? ¿Por qué el Banco conoce que el Conector devuelve json?
Pensando en esto se me ocurre el siguiente refactor de Banco:
Nuestro módulo de alto nivel depende ahora de una abstracción más general, dejándole a los módulos de bajo nivel (Las implementaciones de OperacionesBancarias) todo lo que tiene que ver con la infraestructura (tipo de comunicación, transformación de la respuesta, etc). Pero, ¿no es el token un detalle de bajo nivel? Si la respuesta es afirmativa deberíamos eliminarlo de Banco y OperacionesBancarias devolvería directamente la Cuenta, haciendo que nuestra clase Banco fuera redundante. Sin embargo, yo (que soy el que está programando :P ) creo que cualquier autenticación bancaria me va a devolver un token (hablo desde la ignorancia, pero suena bien) con lo que deja de ser un detalle para formar parte de la abstracción. Eso sí, no estaría mal que fuera una clase Token en lugar de un String, que no todos los tokens tienen porque ser iguales. La clase Banco que no viola el principio de inversión de dependencias quedaría así:
Conclusiones
Yo ya conocía los principios S.O.L.I.D. y se que mis compañeros también (aunque de este código tenemos la culpa Leo y yo :D ). Nos habíamos preocupado de leerlos y de intentar entenderlos mucho antes de dar este curso. Entonces, ¿por qué no fuimos capaces de recordar la dichosa D. incluso habiendo tenido que explicarla el día anterior? Yo pienso que es porque no lo tenemos interiorizado. Hace falta mucha práctica y mucha experiencia trabajando con los principios S.O.L.I.D. en la cabeza para que no se te olviden mientras programas. Por eso considero importante practicar T.D.D. con ejemplos sencillos, porque lo importante no es resolver el problema, lo importante es el proceso mental con el que resuelves el problema.
Criticadme, por favor
Lo que os he contado en este artículo es como entiendo yo el principio de inversión de dependencias. ¿Coincide con lo que entendéis vosotros? Si no es así, ¿en que me he equivocado?
Etiquetas:
dirigidoportests,
pair programming,
SOLID,
TDD
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:
Así que voy a contaros un poco mi motivación. Mi pensamiento tras ver el vídeo de Ben Rady fue:
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
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
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
Etiquetas:
API,
dirigidoportests,
How to,
JUnit,
SOLID
Suscribirse a:
Entradas (Atom)
