Desmitificando los dobles de test: Mocks, stubs and friends

Desmitificando los dobles de test: Mocks, stubs and friends
Sin comentarios Facebook Twitter Flipboard E-mail

Si sientes curiosidad por Agile y eXtreme Programming, es muy probable que hayas escuchado en artículos y charlas que el testing es bueno, que es tu red de protección, que te hace más alto y más guapo, etc etc etc ... con lo que es probable que hayas oído nombrar el término "Mock" o "Mockear dependencias".

En este artículo, vamos a intentar aclarar un poco más la terminología que hay detrás de este y otros conceptos de testing, partiendo de la sabiduría de uno de los grandes: Martin Fowler.

Dobles de test

Fowler, en su artículo "Mocks Aren't Stubs", nos hace ver la necesidad de desambiguar este término del que tanto se abusa en la literatura y sobretodo en frameworks, acabando por convertirse en algo ambiguo y poco claro. Y es que acabamos llamándole a todo "Mock", cuando tenemos una gran variedad de lo que vamos a pasar a llamar a partir de ahora, dobles de test.

Para entenderlo mejor, vamos a partir de una definición que hace el propio Fowler y que deja claro inicialmente cual es el objetivo de los dobles de test:

"Test doubles is a generic term for any kind of pretend object used in place of a real object for testing purposes"

Así pues, un doble de test es simplemente un objeto que utilizamos en sustitución de otro cuando queremos realizar un test y no queremos romper o ir más allá del "sujeto bajo test" (a partir de ahora SUT).

Imaginemos que queremos testear una parte del código que interactúa con el API de pagos de un tercero. Ejercitar este API todo el tiempo puede ser lento, si se produce algún error puedo llegar a pensar que es culpa de mi código y, además, es probable que ni tenga disponible una plataforma de pruebas contra la que poder ejecutar mis tests sin tener que rascarme el bolsillo haciendo un pago cada vez.

Es por esto que resulta conveniente siempre probar en aislamiento el SUT. Este planteamiento, que parece sencillo de explicar, puede resultar bastante complicado en el escenario expuesto anteriormente si hacemos uso de APIs de terceros, por lo que necesito algún mecanismo que me permita contar con dobles, impostores o reemplazos de estas unidades que no quiero ejercitar durante las pruebas. Lo que necesito son pues dobles de test :)

Cuando estoy programando, uno de los principales objetivos que debería tener siempre es que el código que escriba sea lo más mantenible posible. Una manera de conseguirlo es que el código sea expresivo, claro y sencillo de leer y seguir (también conocido como Clean Code). Con esto, quiero remarcar que la intención a la hora de programar es extremadamente importante cuando desarrollamos código compartido en un equipo. En este sentido, en lugar de llamar Mocks a todos los dobles de test, lo que haremos es analizar las diferencias sutiles que existen en cada uno de los tipos y ver cómo podemos aprovecharnos de ellas para mejorar la expresividad del código.

Tipos de dobles de test

Según la clasificación de Fowler, podemos atener a varios tipos de dobles de test:

  • Dummy: Son dobles de test que se pasan allí donde son necesarios para completar la signatura de los métodos empleados, pero no intervienen directamente en la funcionalidad que estamos testeando. Son simplemente relleno.
  • Fake: Son implementaciones de componentes de nuestra aplicación que funcionan y son operativas, pero que sólo implementan lo mínimo necesario para poder pasar las pruebas. No son adecuados para ser desplegados en producción, por lo que nunca serán utilizados fuera del ámbito de un test. En este caso, un ejemplo claro sería una base de datos en memoria.
  • Stubs: Conjunto de respuestas enlatadas que se ofrecerán como resultado de una serie de llamadas a nuestro doble de test. Serían por ejemplo el resultado de una consulta a base de datos que puede realizar un repositorio. Es importante comentar que en este tipo de dobles únicamente atendemos al estado que tienen estos objetos y nunca a su comportamiento o relación con otras entidades.
  • Mocks: Dobles de prueba que son capaces de analizar como se relacionan los distintos componentes, permitiendo verificar si un método concreto ha sido invocado o no, qué parámetros ha recibido o cuantas veces lo hemos ejercitado. Aunque también pueden devolver una respuesta con un estado determinado, su foco se centra más en el análisis del comportamiento. Nos ayudan a testear pues el paso de mensajes entre objetos.

Al igual que pasa con los tests unitarios en los que todos los stacks de programación ya cuentan con su propia xUnit (jUnit para Java, nUnit para .NET, unittest para python, etc), también podemos encontrar multitud de frameworks o librerías para crear dobles de test.

A continuación vamos a ver un par de ejemplos de como usar Stubs y Mocks en JavaScript con Jasmine, ya que ofrece un montón de funcionalidades interesantes para el trabajo con dobles.

Ejemplo de uso de Stubs

En el siguiente ejemplo podemos ver como, a partir de la definición de un repositorio para la consulta de usuarios en la BD, podemos crear un stub que nos permita interactuar con este objeto sin conectar con la BD real.

Recordemos que queremos que nuestros tests sean rápidos e independientes, así que cualquier clase que se ejecute en la frontera o boundary de nuestro sistema, puede ser susceptible de ser reemplazada por un doble de test a la hora de diseñar nuestra pruebas.

Para crear nuestro Stub, jasmine nos ofrece le método spyOn(...).and.callFake con el que vamos a poder definir que cuando se llame a un método concreto de un objeto, este devuelva una respuesta enlatada como resultado.


describe("Locked users", () => {
  let userRepository; 

  beforeEach(() => {
    userRepository = {
      findLockedUsers() {
         return [];
      }
    };

    spyOn(userRepository, "findLockedUsers").and.callFake(() => {
      return [ 'borillo', 'ricardo' ];
    });
  });
  
  it("stubs can be created over functions", () => {
    expect(userRepository.findLockedUsers).toBeDefined();
  });
  
  it("stubs register function calls", () => {
    let lockedUsers = userRepository.findLockedUsers();
    expect(userRepository.findLockedUsers).toHaveBeenCalled();
  });
  
  it("stubs should return custom values", () => {
    let lockedUsers = userRepository.findLockedUsers(); 
    expect(lockedUsers).toEqual([ 'borillo', 'ricardo' ]);
  });
});

Como podemos ver, jasmine no hace ninguna distinción entre Mocks o Stubs, y los llama a todos Spy. Como nuestro objetivo es dejar clara nuestra intención lo máximo posible, podríamos crearnos nuestro propio método de definición del Stub (stubOn) y añadir este plus de semántica al código.

Esta modificación no varía en nada los tests que ya había implementado, pero sí la forma en que se inicializan los valores antes de cada ejecución, por lo que el código quedará de la siguiente forma:


describe("Locked users", () => {
   let userRepository;

   const stubOn = (obj, method, generatorFunction) => {
      spyOn(obj, method).and.callFake(generatorFunction);
   };

   beforeEach(() => {
      userRepository = {
         findLockedUsers() {
            return [];
         }
      };

      stubOn(userRepository, "findLockedUsers", () => {
         return [ 'borillo', 'ricardo' ];
      });
   });

   // Test definitions ...
});  

Ejemplo de uso de Mocks

De forma similar a los Stubs, vamos a poder crear Mocks en jasmine, pero esta vez, tendremos la posibilidad de realizar comprobaciones más orientadas a comportamiento, como son si se ha llamado o no un método, con qué valores ha sido llamado o cuantas veces.

En este caso, para crear un Mock de nuestro repositorio, vamos a utilizar jasmine.createSpyObj, la cual permite definir un esqueleto inicializado con una serie de métodos a los que invocar cuando estemos utilizando el Mock (saveToStorage y remove).

Con el fin de detectar las llamadas que se realizan a este Mock, lo inyectaremos como dependencia al userService que hemos definidiso, asertando posteriormente si se han realizado o no las llamadas y qué valores se han pasado.


describe("User registration", () => {
   let userRepository, userService;

   beforeEach(() => {
      userRepository = jasmine.createSpyObj(
         'userRepository', ['saveToStorage', 'remove']
      );

      userService = {
         register(userRepository, userName) {
            userRepository.saveToStorage('db');
            userRepository.saveToStorage('disk');
         }
       };

       userService.register(userRepository, 'borillo');
   });

   it("mock functions should be available", () => {
      expect(userRepository.saveToStorage).toBeDefined();
   });

   it("mock functions should be called", () => {
      expect(userRepository.saveToStorage).toHaveBeenCalled();
      expect(userRepository.remove).not.toHaveBeenCalled();
   });

   it("mock functions should receive params", () => {
      expect(userRepository.saveToStorage).toHaveBeenCalledWith('db');
      expect(userRepository.saveToStorage).toHaveBeenCalledWith('disk');
   });

   it("mock functions should be called several times", () => {
      expect(userRepository.saveToStorage.calls.count()).toEqual(2);
   });
});

Conclusión

Nuestros tests deben ser rápidos, independientes, expresivos y transmitir la intención de forma clara y concisa.

A la hora de crear tests que no dependan de otros colaboradores o incluso de infraestructura externa como la BD, los dobles de test pueden ser nuestros mejores aliados.

Su uso tanto para la gestión del estado con los Stubs, como del comportamiento con los Mocks, son una herramienta más que no podemos olvidar en nuestro toolkit de Agile Developers.

Referencias

Comentarios cerrados
Inicio