Un ejemplo de doble de test con spies

Imagen de Imagen de https://www.organicconsumers.org

Introducción

En el artículo anterior hablé sobre los dobles de test y los dobles de test de tipo stubs. Los stubs son los tipos de dobles de test que más utilizo en mi día a día.
En este artículo voy a hablar sobre otro tipo de dobles de test: spies.

Tipos de test unitarios

Una forma de clasificar los test unitarios es a partir de la forma que comprueban que nuestro código de producción funciona. Así, tenemos test que comprueban el estado y test que comprueban el comportamiento.

  • Estado: Estos test primero realizan operaciones y comprueban que el resultado es el esperado. La comprobación de que el resultado es el esperado se hace a través de los métodos de tipo assert, como por ejemplo assertEquals, assertTrue y assertNull. Como puedes imaginarte, este tipo de test unitario es el más común y un porcentaje muy alto de los test son de este tipo.
  • Comportamiento: Estos test lo que hacen es comprobar que se llaman a ciertos métodos de los objetos colabores de la clase que estamos testeando. Lo que comprueban es la interacción de nuestro método con los colaboradores. Los dobles de test de tipo spies nos ayudan a realizar esta comprobación, es decir, nos ayudan a testear el comportamiento de nuestro código de producción.

Spies: un ejemplo

Los tipos de test spies son objetos que graban información de como han sido llamados.
Vamos a verlo con un ejemplo. Primero, vas a ver el código de producción y luego el código de test.

Tenemos un método findOrCreateWith de la clase UserService, que devuelve un usuario a partir de su email.
Si el usuario con ese email existe en la base de datos, lo devuelve. Pero si no existe, lo añade a la base de datos y posteriormente envía un email a esa dirección.
Entonces, si existe un usuario con ese email lo devuelve, pero si no existe lo crea y envía un email.

Algunos comentarios sobre el código de arriba:

  • UserService tiene dos objetos colaboradores: UserDAOEmailService.
  • UserDAO se encarga de manejar el usuario en la base de datos.
  • EmailService se encarga de enviar emails.
  • UserDAO y EmailService son interfaces.
  • El método UserDAO.findUserBy siempre devuelve un objeto de tipo User. El método isEmpty de User devuelve true cuando es un objeto vacío, ya que no está en la base de datos, y false cuando es el objeto real. No me gusta devolver null en ningún método por lo que utilizo este patrón. Si quieres saber más escribí un artículo sobre esto aquí.

Después de ver el código de producción, lo que queremos es comprobar que cuando el usuario no existe en la base de datos enviamos un email al usuario, o lo que es lo mismo llamamos al método sendEmailTo del colaborador EmailService.

En el test should_send_an_email_when_a_user_was_created los pasos que he seguido y que separado con un línea en blanco son:

  1. Inyectar un stub de tipo UserDAO y un spy de tipo EmailService a UserService.
  2. Llamar al método que estamos testeando: findOrCreateWith.
  3. Finalmente, comprobar que se ha enviado el email. Esto se hace comprobando que el método wasSentEmail vale true. Y la única manera de que wasSentEmail sea true es que sendEmailTo halla sido llamado.

 

Mejoras

No estoy del todo contento con el código de producción que he escrito, así que si alguien me puede ayudar a mejorarlo estaré más que contento en leerlo. Los siguientes puntos describen algunas de las partes que son mejorable bajo mi punto de vista:

  • findOrCreateWith es un nombre horrible para llamar un método. Quizás hubiera sido mejor el llamarlo find o create.
  • EmailService tampoco me gusta como nombre. Quizás SenderEmail hubiera sido mejor.
  • Es importante que en un mismo método halla el mismo nivel de abstracción o lo que es lo mismo, que se utilice el mismo lenguaje. Creo que en este caso findOrCreateWith no está en el mismo nivel. Una pista que me hace pensar eso es que la clase que estamos testeando se llama UserService e inyectamos un objeto que se llama de forma parecida: EmailService.

El código de test no es tampoco perfecto, pero una de las razones es porque lo que he intentado simplificar al máximo.

Fuentes

  • http://googletesting.blogspot.nl/2013/03/testing-on-toilet-testing-state-vs.html
  • http://www.martinfowler.com/bliki/TestDouble.html

 

Un ejemplo de doble de test con stubs

Stub
Imagen de http://www.parts-recycling.com

Introducción

Un doble de test simula un objeto de producción y nos permite el probar otros objetos.

Según Martin Fowler en este artículo, hay cinco tipos de dobles de test: dummy, fake, stubs, spies y mocks.

En este artículo voy a enfocarme en stubs y en fakes dummies porque son los dobles de test que más suelo utilizar.
Para saber más sobre los dobles de test podéis leer el artículo de Fowler o este de XUnitPatterns, que son muy buenos.

Aunque podemos crear un objeto de tipo stub con cualquier framework de pruebas como JMock o EasyMock, es mejor hacerlo nosotros mismos para así entender mejor como funcionan.

Ventajas de los dobles de test

Los dobles de test unitarios tienen dos ventajas frente a utilizar los objetos reales de producción:

  • Determinismo: siempre se va a comportar como queremos. Con los objetos reales dependiendo de muchos factores van a comportarse de una forma u otra. Así, si el objeto accede a base de datos se comportará de forma distinta dependiendo de los registros que hay en la base de datos.
  • Rapidez: no acceden a la base de datos o al sistema de ficheros y tampoco acceden a Internet por lo que hacen que el test sea mucho más rápido, que es lo que queremos. Porque unos de los principales cualidades de un buen test unitario es que es rápido porque así podemos ejecutarlo a menudo.

 

Stubs

Stubs, cuya traducción al español no me atrevo a hacer, son objetos que han sido programados de forma muy simple por nosotros para que responden de una forma determinada.

Imagina que tenemos un objecto UserService que tiene un método que se llama signIn que permite validar que un usuario pueda hacer “log in” y devuelve el usuario.

En el código anterior, el método signIn solo hace tres cosas: buscar al usuario a partir de su email, validar que las contraseñas son las mismas y devolver el usuario.

Lo que queremos probar es que si el usuario tiene diferente contraseña que la pasada por parámetro se va a lanzar una excepción de tipo AuthenticationException.
Para ello, queremos que UserDAO devuelva un usuario con diferente contraseña que la que pasamos por parámetro.
Vamos a crear una clase de implemente UserDAO y cuyo método findUserBy devuelva siempre el mismo usuario.

El nombre del objeto de tipo stub es StubUserDAO y como puedes comprobar no tiene ningún misterio. Es una clase que implementa UserDAO cuyo único método devuelve un usuario con la contraseña “other_password”.

El método initialization crea los objetos UserService y StubUserDAO e inyecta el objeto StubUserDAO a UserService.

En el ejemplo, hay dos test unitarios que hacen lo mismo pero de dos formas distintas. Ambos comprueban que se lanza la excepción AuthenticationException.

El test should_throw_exception_when_passwords_are_different_version1 comprueba que se lanza la exception con un fail cuando no se ha lanzado.
El test should_throw_exception_when_passwords_are_different_version2 lo comprueba a través del estándar JUnit.

Dummy

Dummy es otro tipo de doble de test que no hace nada. Se suele utilizar para no tener que pasar como parámetro un null como valor.

En el ejemplo anterior el objeto EMAIL es un doble de test de tipo dummy. El código de producción que implementa la interfaz UserDAO lo utiliza, pero como estamos utilizando nuestro stub pues no se utiliza.
En el ejemplo, si EMAIL valiera null no pasaría nada pero eso es porque no es código real de producción.
Suelo comprobar que los parámetros (email y password en este ejemplo) del método no valgan null en mi código de producción.

5 consejos para escribir mejores test unitarios

Cuando empecé a escribir test unitarios cometí muchos errores y seguro que todavía cometo y cometeré muchos otros. En este artículo quiero ayudar a la gente que empieza a que no cometan los mismos o al menos a que puedan identificarlos.

1. Una razón para fallar

Cuando un test falla tiene que estar muy claro cual es la razón por la que ha fallado y si nuestro test unitario esta comprobando mas de una cosa, tendremos que mirar todas esas partes del código y eso nos va a costar mas tiempo de lo necesario.
Por el contrario, si solo comprueba una parte específica de nuestro código de producción, va a resultar trivial el solucionarlo cuando el test falle.
Como regla general, tener solo un assert, es la mejor forma de cumplir esta buena práctica.

2. Nombre del test

El nombre del test es muy importante, ya que al ejecutarlo lo primero que vas a ver cuando falle es el nombre. Así que, el nombre del test (junto con el mensaje de error) debe de decirte el porqué ha fallado sin tener que ver el código de producción.
El nombre tiene que contener el qué se está testeando y cual es el resultado que se espera. Como puedes imaginarte, al contener esta información el nombre del test no va a ser corto. Por lo que se podría decir que, al contrario que con el código de producción, el nombre va a ser largo.
En mi caso, utilizo el siguiente formato:

should_elResultadoEsperado_when_elCasoQueEstoyProbando

Un ejemplo del formato que utilizo seria el siguiente:

should_throw_exception_when_there_is_no_internet_connection

Hay mucha gente que en vez de utilizar un guión bajo para separar las palabras pone en mayúsculas la primera letra de la palabra:

shouldThrowExceptionWhenThereIsNoInternetConnection

No hay ninguna opción que sea la mejor. Utilizo guiones bajos porque personalmente entiendo mejor el texto.

3. Rápido

Que tus test unitarios se ejecuten en pocos milisegundos es una de las cualidades más importantes que deben tener.
Y te preguntarás el porqué es tan importante que tus test unitarios sean rápidos. Principalmente, porque podrás ejecutarlos cada pocos minutos y eso te permitirá el conocer si hay algo que no funciona como esperas, casi al momento de escribirlo.
El principal problema de velocidad en los test es debido a que el código habla con la base de datos, con el sistema de archivos o que acceden a internet.
Para solucionar este problema puedes utilizar dobles de test, en vez de la clase que realiza este tipo de operaciones que tardan tanto en ejecutarse.
Escribir dobles de test es fácil pero en general se suelen utilizar librerías que hacen el trabajo por ti y te dan más opciones. En Java tienes muchas. En mi caso, utilizo JMock.

4. Fixture

Ten mucho cuidado al compartir variables y métodos entre test. Más de una vez no entendía porque un test fallaba o porque un test no fallaba y la razón era porque tenía compartía variables entre varios test unitarios.
La mejor forma de evitar esto es no compartir variables entre test. También se pueden compartir variables, pero para evitar estos problemas, inicializa las variables compartidas en el método setUp de la clase.

5. Test que no comprueba nada

Cuando veas el código de tu test tiene que estar más que claro cual es la manera de que falle el test. Por eso, si tienes un test sin ningún assert y sin manejar las excepción, ¿como vas a saber cuál es la razón de que ha fallado?
Creo que todo test tiene que tener de forma explícita cual es su razón de fallar.
Así, un test que no comprueba el resultado, que no verifica que un objeto colaborador se ha llamado o que no maneja las excepciones no tiene mucho significado.