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