jasmine: 3. spies, stubs y mocks

En esta entrada veremos qué son los spies y cómo funcionan en jasmine

Lawrence Alma-Tadema

archivado en: JavaScript / 26 febrero, 2016 / taller:

Como vimos, en jasmine, cada test se denomina suite y analiza una parte concreta del código, como un objeto o una clase. Las suites agrupan una o más funciones llamadas describe, en las que se definen distintos expects, es decir, funciones en las que se evalúa si algo tiene el valor esperado.

Lo que se evalúa se conoce como match y traducido sería algo así como "espero que (esto).sea(estoOtro)". Se pueden definir matches personalizados, pero por lo general basta con los que trae de fábrica, que son:

  • toEqual: es igual a algo.
  • toBe: es y es como algo
  • toBeTruthy: es verdadero (truthy)
  • toBeFalsy: es falso (falsy)
  • toContain: contiene algo
  • toBeDefined: está definido
  • toBeUndefined: es undefined
  • toBeNul: es nulo
  • toBeNaN: no es un número
  • toBeGreaterThan: es mayor que algo
  • toBeLessThan: es menor que algo
  • toBeCloseTo: un número se aproxima a algo cuando se redondea
  • toMatch: se encuentra la expresión regular
  • toThrow: se espera que dé una excepción
  • Y, además, con el not, se puede negar todo lo anterior.

Además, vimos que podemos definir cosas antes de cada expect mediante la función beforeEach, una acción que se conoce como setup, y resetearlas en la función afterEach, que en argot se denomina teardown.

Repasado esto, vamos a ver qué son y cómo se utilizan los spies, que son otro concepto fundamental en jasmine y en general en los test.

Spies, stubs y mocks

Los test unitarios deben ser lo más atómicos posible y cada prueba debería evaluar partes muy concretas del código sin afectar los datos originales. Sin embargo, normalmente el código es complejo y los métodos se entrelazan unos con otros en dependencias que pueden llegar a ser muy enrevesadas. Es más, si estamos siguiendo una metodología TDD o BDD puede ocurrir también que tengamos que testar algo que tenga aún partes por programar o que dependa de la integración con una api rest que vete a saber cuándo la terminarán los de back o, al contrario, que modifique la base de datos y no está replicada... Para este tipo de escenarios, en el mundo de los test se manejan tres artilugios llamados spies, stubs y mocks, que en esencia vienen a ser objetos que simulan el comportamiento de algunas partes del código. Se diferencian por la manera en que afectan al objeto, clase o función original:

  • Los spies no alteran el original, sino que se limitan a observar cosas, como el número de veces que se pasa por un método o la cantidad de argumentos que recibe.
  • Los stubs se comportan como los spies, pero además reemplazan alguna funcionalidad original, como el resultado que se devuelve después de un proceso. Por ejemplo, puede interesarnos stubear todo lo que tenga que ver con la persistencia de datos y reemplazar las llamadas rest con estos métodos fake.
  • Los mocks, en cambio, sustituyen por completo al original.

A diferencia de otros frames más sofisticados, como sinon js, en jasmine no distinguen sintácticamente unos de otros, sino que usan lo que denominan spie para todo, aunque en realidad se esté stubeando o mockeando, por lo que me remito al artículo Best Practices for Spies, Stubs and Mocks in Sinon.js de Jani Hartikainen, autor de codeutopía, para quien esté interesado en profundizar sobre el tema.

Este tipo de artilugios -los spies, stubs y demás- reciben el nombre de dobles, porque al igual que los dobles de las películas reemplazan al actor principal.

Los spies de jasmine

Para ver cómo funcionan los spies en jasmine imaginemos que tenemos que testar una aplicación para gestionar los libros de una biblioteca. Hay miles de líneas de código, pero solo debemos preocuparnos por la parte final del proceso de dar de alta los libros. En concreto, tenemos que comprobar este caso de uso: «Se debe poder guardar el título, el autor y el año de un libro. El autor debe estar en minúsculas y el año debe ser un número». Aunque, como expliqué, lo suyo sería escribir primero el test y luego el código de la aplicación, en este caso creo que quedará más clara la explicación a la inversa, así que antes que nada echemos un vistazo al código que vamos a testar. Algo así:

var AddBook = function() {

/* Recogemos el libro */

this.getBook = function(title, author, year) {

/* Ponemos el autor en minúscula */

author = this.prepareAuthor(author);

/* Comprobamos que están todos los campos y que el año es un número */

if ( !this.checkBook(title, author, year) ) {

return false;

}

/* Enviamos el libro a la bbdd */

this.sendBook(title, author, year);

return true;

};

this.checkBook = function(title, author, year) {

var isValid = true;

if ( !title || !author || !year || isNaN(year) ) {

isValid = false;

}

return isValid;

};

this.prepareAuthor = function(author) {

author = author.toLowerCase();

return author;

};

this.sendBook = function(title, author, year) {

// ajax request...

};

};

Lo primero que podemos comprobar es que existe un método para recoger los datos iniciales y que ese método está preparado para recibir los tres argumentos que nos han pedido. Y para eso vamos a preparar un spie, que en jasmine se puede construir de dos maneras. La más sencilla es la fórmula:

spyOn(objeto, 'miMetodo').

Además, nos va a venir de perlas usar alguno de los dos métodos que tiene jasmine para comprobar si se lanza un método del objeto espiado:

  •  toHaveBeenCalled: que devuelve true si se ha lanzado
  • toHaveBeenCalledTimes: true si se ha lanzado n veces.

Como nos basta con una vez, usamos el primero.

describe('add book to library', function() {

var testBook;

/* Antes de cada expect, instanciamos el objeto que estamos testando */

beforeEach(function() {

testBook = new AddBook();

/* Creamos el espía */

spyOn(testBook, "getBook");

/* llamamos al método */

testBook.prepareAuthor();

});

it('should be possible called getBook method', function() {

expect(testBook.getBook).toHaveBeenCalled();

});

});

Test superado, ya que el método existe. Sin embargo, esto nos habría dado error.

...

// Error: metodoInexistente() method does not exist

spyOn(testBook, "prepareAuthor");

testBook.prepareAuthor();

expect(testBook.metodoInexistente).toHaveBeenCalled();

...

Ahora nos falta comprobar si ese método es capaz de recibir los tres parámetros que nos piden y para eso tenemos el método toHaveBeenCalledWith, que da true si se han enviado los argumentos indicados.

describe('add book to library', function() {

var testBook;

beforeEach(function() {

testBook = new AddBook();

spyOn(testBook, 'getBook');

testBook.getBook('La diosa blanca', 'Robert Graves', 1948);

});

it('should be possible called getBook method with 3 params', function() {

expect(testBook.getBook).toHaveBeenCalledWith('La diosa blanca', 'Robert Graves', 1948);

});

});

En realidad, este último test nos daría un falso positivo, ya que en javaScript los métodos no cascan si se les envía parámetros de más o de menos y esto se usa más bien para comprobar si se ha llamado a un método con tal o cual parámetro, por ejemplo, como resultado de un if else.

A través

La manera anterior de crear un Spie es como sacar una copia fantasma, una carcasa vacía, en la que solo podemos comprobar si existe un método. Para comprobar cosas más sofisticadas, como el resultado, necesitamos concatenar «cláusulas», funciones, al Spie, con las que le pasamos todo o parte del comportamiento original. Estas son:

1. and.callThrough()

Copia el método, lo que permite saber cuál cuál es el resultado del proceso que realiza. Por ejemplo, así podríamos comprobar si funciona el método original para validar los parámetros, que recordemos devuelve true si están bien y false en caso contrario.

...

it('should be possible check params are valid', function() {

var result;

spyOn(testBook, 'checkBook').and.callThrough();

result = testBook.checkBook('La diosa blanca', 'Robert Graves', 1948);

expect(result).toBeTruthy();

});

it('should be possible check year is not a number', function() {

var result;

spyOn(testBook, 'checkBook').and.callThrough();

result = testBook.checkBook('La diosa blanca', 'Robert Graves', 'sin fecha');

expect(result).toBeFalsy();

});

...

2. and.returnValue() y and.returnValue(s)

Copia la función, pero fuerza a que devuelva determinados(s) resultado(s). En nuestro caso tenemos una llamada ajax que envía el libro a la base de datos, pero, además de que así la modificaríamos en caso de que no estuviera replicada, no nos interesa comprobar en este spec cómo funciona el api rest, el back, sino solo el proceso de validación y envío front, por lo que podemos stubearla y forzar el resultado, por ejemplo, para mostrar al usuario un mensaje de que el libro ha sido indexado correctamente.

it('should be possible called sendBook', function() {

var result;

spyOn(testBook, 'sendBook').and.returnValue(true);

result = testBook.sendBook();

expect(result).toBeTruthy();

});

Este ejemplo es un poco chorra, pero en la próxima entrada de este taller veremos cómo testar de forma más precisa las llamadas ajax y demás procesos asíncronos.

3. and.callFake()

Con este método fakeamos el método, es decir, cambiamos lo que hace por otra cosa. Por ejemplo, imaginemos que en el código original aún no está implementado el método prepareAuthor que pone en minúsculas al autor, pero en nuestra parte del test necesitamos que ese proceso ya esté operativo, así que podemos fakear el método.

it('should be possible called sendBook', function() {

var title = 'La diosa blanca',

      author = 'Robert Graves',

      year = 1948;

var fakePrepare = function(author) {

return author.toLowerCase();

};

spyOn(testBook, 'prepareAuthor').and.callFake(fakePrepare);

author = testBook.prepareAuthor(author);

spyOn(testBook, 'sendBook').and.returnValue(true);

result = testBook.sendBook(title, author, year);

expect(result).toBeTruthy();

});

Podemos recuperar en cualquier momento el comportamiento del método original con and.stub().

spyOn(testBook, 'prepareAuthor').and.callFake(fakePrepare);

testBook.prepareAuthor.and.stub();

4. and.throwError()

Este método se utiliza combinado con el match toThrowError y, como se deduce, sirve para forzar un error. No necesita mayor aclaración.

Bueno, quedan más cosas por explicar de jasmine, pero como introducción espero que baste. Si acaso más adelante, cuando hayamos visto otras herramientas como mocha o sinon, retomo el tema. De momento, en lo que atañe a jasmine, lo dejo aquí.

|| Tags: , , ,

valoración de los lectores sobre jasmine: 3. spies, stubs y mocks

  • estrellica valoración positiva
  • estrellica valoración positiva
  • estrellica valoración positiva
  • estrellica valoración positiva
  • estrellica valoración positiva
  • 5 sobre 5 (7 votos)

¿Te ha parecido útil o interesante esta entrada?
dormido, valoración 1 nadapensativo, valoración 2 un poco sonrisa, valoración 3 a medias guiño, valoración 4 bastante aplauso, valoración 5 mucho

Tú opinión es muy importante, gracias por compartirla!

Los comentarios están cerrados.