Mocks de clase ES6
Jest se puede utilizar para crear mocks que simulan clases de ES6 que importan a los archivos que deseas probar.
Las clases de ES6 son funciones constructor con un poco de sintaxis adicional. Por lo tanto, cualquier mock para una clase ES6 debe ser una función o una clase ES6 actual (que es, de nuevo, otra función). Entonces puedes hacer un mock usando funciones mock.
Un ejemplo de clase ES6
Usaremos un ejemplo de una clase que reproduce archivos de sonido,Reproductor
y una clase de consumidores que una esa clase,Consumidor
. Crearemos un mock de Reproductor
en nuestros test de Consumidor
.
// reproductor.js
export default class Reproductor{
constructor() {
this.variableEjemplo = 'valorEjemplo';
}
reproducirArchivoDeSonido(nombreDeArchivo) {
console.log('Reproduciendo archivo de sonido ' + nombreDeArchivo);
}
}
// consumidor.js
import Reproductor from './reproductor';
export default class Consumidor {
constructor() {
this.reproductor = new Reproductor();
}
reproduceAlgoCool() {
const nombreDeArchivoCool = 'cancion.mp3';
this.reproductor.reproducirArchivoDeSonido(nombreDeArchivoCool);
}
}
Las 4 formas de crear una clase mock ES6
Mocks automáticos
jest.mock('./reproductor')
regresa un mock automático que se puede ocupar para espiar llamadas al constructor de la clase y todos sus métodos. Este reemplaza la clase ES6 con un constructor mock, y reemplaza todos sus métodos con funciones mock que siempre regresan undefined
. Las llamadas a los métodos se almacenan en elMockAutomatico.mock.instances[índice].nombreDelMetodo.mock.calls
.
Ten en cuenta que si utilizas funciones de flecha (=>) en tus clases, estas NO serán parte del mock. Esto es porque las funciones flecha no están presentes en el prototipo del objeto, son simplemente propiedades que contienen una referencia a una función.
Si no necesitas reemplazar la implementación de la clase, esta es la opción más fácil para configurar. Por ejemplo:
import Reproductor from './reproductor';
import Consumidor from './consumidor';
jest.mock('./reproductor'); // Reproductor es ahora un constructor mock
beforeEach(() => {
// Borra todas las instancias y llamadas al constructor y a todos los métodos:
Reproductor.mockClear();
});
it('Podemos verificar que el consumidor llamó al constructor de la clase', () => {
const consumidor = new Consumidor();
expect(Reproductor).toHaveBeenCalledTimes(1);
});
it('Podemos verificar que el consumidor llamó a algún método de la instancia', () => {
// Muestra que mockClear() está funcionando:
expect(Reproductor).not.toHaveBeenCalled();
const consumidor = new Consumidor();
// Constructor debió haber sido llamado nuevamente:
expect(Reproductor).toHaveBeenCalledTimes(1);
const nombreDeArchivoCool = 'cancion.mp3';
consumidor.reproduceAlgoCool();
// mock.instances esta disponible con mocks automáticos:
const instanciaMockDeReproductor= Reproductor.mock.instances[0];
const mockDeReproducirArchivoDeSonido = instanciaMockDeReproductor.reproducirArchivoDeSonido;
expect(mockDeReproducirArchivoDeSonido.mock.calls[0][0]).toEqual(nombreDeArchivoCool);
// Equivalente a la verificación anterior:
expect(mockDeReproducirArchivoDeSonido).toHaveBeenCalledWith(nombreDeArchivoCool);
expect(mockDeReproducirArchivoDeSonido).toHaveBeenCalledTimes(1);
});
Mock Manual
Crea un mock manual almacenando una implementación mock en la carpeta __mocks__
. Esto te permite especificar la implementación, y puede ser utilizada a través de varios archivos de test.
// __mocks__/reproductor.js
// Importa esta exportación nombrada en tu archivo test:
export const mockDeReproducirArchivoDeSonido = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {reproducirArchivoDeSonido: mockDeReproducirArchivoDeSonido};
});
export default mock;
Importa la simulación y el método de simulación compartido por todas las instancias:
// consumidor.test.js
import Reproductor, {mockReproducirArchivoDeSonido} from './reproductor';
import Consumidor from './consumidor';
jest.mock('./reproductor'); // Reproductor es ahora un constructor mock
beforeEach(() => {
// Borra todas las instancias y llamadas al constructor y a todos los métodos:
Reproductor.mockClear();
mockReproducirArchivoDeSonido.mockClear();
});
it('Podemos verificar que el consumidor llamó al constructor de la clase', () => {
const consumidor = new Consumidor();
expect(Reproductor).toHaveBeenCalledTimes(1);
});
it('Podemos verificar que el consumidor llamó a algún método de la instancia', () => {
const consumidor = new Consumidor();
const nombreDeArchivoCool = 'cancion.mp3';
consumidor.reproduceAlgoCool();
expect(mockDeReproducirArchivoDeSonido).toHaveBeenCalledWith(nombreDeArchivoCool);
});
jest.mock()
con el parámetro de fábrica de módulo
Llamando jest.mock(ruta, fabricaDeModulo)
toma un argumento fabrica de modulo. Una fábrica de módulo es una función que regresa un mock.
Para crear un mock de una función constructor, la fábrica de módulo debe regresar una función constructor. En otras palabras, la fábrica de módulo debe ser una función que regresa una función - una función de alto orden, o HOF, por sus siglas en inglés (Higher-Order Function).
import Reproductor from './reproductor';
const mockReproducirArchivoDeSonido = jest.fn();
jest.mock('./reproductor', () => {
return jest.fn().mockImplementation(() => {
return {reproducirArchivoDeSonido: mockReproducirArchivoDeSonido};
});
});
Una limitación el argumento fábrica de modulo es que, ya que las llamadas a jest.mock()
son elevadas hasta el inicio del archivo, no es posible definir una variable y luego usarla en la fábrica. Se hace un excepción para variables que empiecen con la palabra 'mock'. ¡Depende de ti garantizar de que serán inicializadas a tiempo! Por ejemplo, lo siguiente arrojará un error fuera de alcance (out-of-scope) debido al uso de 'fake' en lugar de 'mock' en la declaración de la variable:
// Nota: esto fallará
import Reproductor de './reproductor';
const fakeReproducirArchivoDeSonido = jest.fn();
jest.mock('./reproductor, () => {
return jest.fn().mockImplementation(() => {
return {reproducirArchivoDeSonido: fakeReproducirArchivoDeSonido };
});
});
mockImplementation()
o mockImplementationOnce()
Sustituir el mock utilizando Puedes reemplazar todos los mocks anteriores para cambiar la implementación, para uno así como para todos los test, al llamar mockImplementation()
en el mock existente.
Las llamadas a jest.mock son elevadas al principio del código. Puedes especificar una mock posteriormente, por ejemplo, en beforeAll()
, al llamar mockImplementation()
(o mockImplementationOnce()
) en el mock existente en lugar de usar el parámetro de fábrica. Esto también le permite cambiar la simulación entre pruebas, si se necesita:
import Reproductor de './reproductor';
import Consumidor de './consumidor';
jest.mock('./reproductor');
describe('Cuando Reproductor arroja un error', () => {
beforeAll(() => {
Reproductor.mockImplementation(() => {
return {
reproducirArchivoDeSonido: () => {
throw new Error('Error de prueba');
},
};
});
});
it('Debería arrojar un error al llamar a reproduceAlgoCool', () => {
const consumidor = new consumidor();
expect(() => consumidor.reproduceAlgoCool()).toThrow();
});
});
A profundidad: Entendiendo las funciones constructor mock
Construir tus funciones de constructor de mock utilizando jest.fn().mockImplementation()
hace que los mock se vean más complicados de lo que en realidad son. Esta sección muestra cómo puedes crear tus propios mock para ilustrar cómo funciona el simular módulos con mocks.
Mock manual de otra clase ES6
Si defines una clase ES6 utilizando el mismo nombre de archivo que la clase mock en la carpeta __mocks__
, éste servirá como el mock. Esta clase será utilizada en lugar de la clase real. Esto te permite inyectar una implementación de prueba para la clase, pero no proporciona una forma de espiar las llamadas.
Para un ejemplo ideado, la proyección podría verse así:
// __mocks__/sound-player.js
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
}
playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
}
}
Mock simple utilizando un parámetro de fábrica de módulo
La función de fábrica de módulo pasada a jest.mock(ruta, fabricaDeModulo)
puede ser una función de alto orden que regresa una función*. Esto permitirá llamar a new
en la simulación. De nuevo, esto te permite inyectar un comportamiento diferente para las pruebas, pero no proporciona una forma de espiar llamadas.
* La función de fábrica del módulo debe devolver una función
Para crear un mock de una función constructor, la fábrica de módulo debe regresar una función constructor. En otras palabras, la fábrica de módulo debe ser una función que regresa una función - una función de alto orden, o HOF, por sus siglas en inglés (Higher-Order Function).
jest.mock('./reproductor', () => {
return function () {
return {reproducirArchivoDeSonido: () => {}};
};
});
Nota: las funciones de flecha no funcionaran
Ten en cuenta que la simulación no puede ser una función de flecha porque no se permite llamar a new
en una función de flecha en JavaScript. Así que esto no funciona:
jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions can't be called with new
return {playSoundFile: () => {}};
};
});
Esto arrojará TypeError: _soundPlayer.default is not a constructor (Error de tipo: soundPlayer.default no es un constructor), a menos que el código sea transpilado a ES5, por ejemplo por @babel/preset-env
. (ES5 no tiene funciones de flecha ni clases, por lo que ambas serán transpiladas a funciones simples.)
Haciendo seguimiento del uso (espiando al mock)
Inyectar una implementación de prueba es útil, pero probablemente desearás probar si el constructor de clase y los métodos están siendo llamados con los parámetros correctos.
Espiando al constructor
Para rastrear llamadas al constructor, reemplaza la función devuelta por la función de alto orden con una función de mock de Jest. Créalo con jest.fn()
, y luego especifica su implementación con mockImplementation()
.
import SoundPlayer from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
});
});
Esto nos permitirá inspeccionar el uso de nuestra clase mock, utilizando Reproductor.mock.calls
: expect(Reproductor).toHaveBeenCalled();
o su equivalente cercano expect(Reproductor.mock.calls.length).toEqual(1);
Creando mocks para exports no default
Si la clase no es la exportación default del módulo, entonces necesitas devolver un objeto con un atributo llamado cómo exportación de la clase.
import {Reproductor} from './reproductor';
jest.mock('./reproductor', () => {
// Funciona y te permite verificar llamadas al constructor:
return {
Reproductor: jest.fn().mockImplementation(() => {
return {reproducirArchivoDeSonido: () => {}};
}),
};
});
Espiando métodos de nuestra clase
Nuestra clase simulada necesitará proporcionar cualquier función miembro (playSoundFile
en el ejemplo) que será llamada durante nuestras pruebas, o bien obtendremos un error por llamar una función que no existe. Pero probablemente querramos tambien espiar llamadas a esos métodos, para asegurar de que fueron llamados con los parámetros esperados.
Un nuevo objeto será creado cada vez que la simulación de la función constructora sea llamada durante las pruebas. Para espiar llamadas de método en todos estos objetos, poblamos playSoundFile
con otra función de simulación, y almacenamos una referencia para esa misma función de simulación en nuestro archivo de prueba, para que esté disponible durante las pruebas.
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
// Now we can track calls to playSoundFile
});
});
El equivalente de simulación manual de esto sería:
// __mocks__/sound-player.js
// Import this named export into your test file
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
El uso es similar a la función de fábrica del módulo, sólo que puede omitir el segundo argumento de jest.mock()
, y deberá importar el método simulado a su archivo de prueba, puesto que ya no es definido ahí. Utilice la ruta de módulo original para esto; no incluya __mocks__
.
Limpiando entre pruebas
Para vaciar el registro de llamadas a la función constructura de simulaciones y sus métodos, llamamos a mockClear()
en la función beforeEach()
:
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
Ejemplo completo
Aquí tiene un archivo de prueba completo el cual utiliza un parámetro de fábrica de módulos para jest.mock
:
// sound-player-consumer.test.js
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
it('The consumer should be able to call new() on SoundPlayer', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
// Ensure constructor created the object:
expect(soundPlayerConsumer).toBeTruthy();
});
it('We can check if the consumer called the class constructor', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it('We can check if the consumer called a method on the class instance', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
});