Skip to main content
Version: 29.7

Mocks de clase ES6

Jest se puede utilizar para simular 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 simulación para una clase ES6 debe ser una funcion o una clase ES6 actual (que es, de nuevo, otra función). So you can mock them using mock functions.

Un ejemplo de clase ES6

We'll use a contrived example of a class that plays sound files, SoundPlayer, and a consumer class which uses that class, SoundPlayerConsumer. We'll mock SoundPlayer in our tests for SoundPlayerConsumer.

sound-player.js
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}

playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
}
sound-player-consumer.js
import SoundPlayer from './sound-player';

export default class SoundPlayerConsumer {
constructor() {
this.soundPlayer = new SoundPlayer();
}

playSomethingCool() {
const coolSoundFileName = 'song.mp3';
this.soundPlayer.playSoundFile(coolSoundFileName);
}
}

Las 4 formas de crear una clase mock ES6

Mocks automáticos

Calling jest.mock('./sound-player') returns a useful "automatic mock" you can use to spy on calls to the class constructor and all of its methods. It replaces the ES6 class with a mock constructor, and replaces all of its methods with mock functions that always return undefined. Method calls are saved in theAutomaticMock.mock.instances[index].methodName.mock.calls.

note

If you use arrow functions in your classes, they will not be part of the 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 SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor

beforeEach(() => {
// Clear all instances and calls to constructor and all methods:
SoundPlayer.mockClear();
});

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', () => {
// Show that mockClear() is working:
expect(SoundPlayer).not.toHaveBeenCalled();

const soundPlayerConsumer = new SoundPlayerConsumer();
// Constructor should have been called again:
expect(SoundPlayer).toHaveBeenCalledTimes(1);

const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();

// mock.instances is available with automatic mocks:
const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
expect(mockPlaySoundFile.mock.calls[0][0]).toBe(coolSoundFileName);
// Equivalent to above check:
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});

Mock Manual

Create a manual mock by saving a mock implementation in the __mocks__ folder. Esto te permite especificar la implementación, y puede ser utilizada a través de varios archivos de test.

__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;

Importa la simulación y el método de simulación compartido por todas las instancias:

sound-player-consumer.test.js
import SoundPlayer, {mockPlaySoundFile} from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor

beforeEach(() => {
// Clear all instances and calls to constructor and all methods:
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});

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).toHaveBeenCalledWith(coolSoundFileName);
});

Calling jest.mock() with the module factory parameter

jest.mock(path, moduleFactory) takes a module factory argument. 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 SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
caution

Since calls to jest.mock() are hoisted to the top of the file, Jest prevents access to out-of-scope variables. By default, you cannot first define a variable and then use it in the factory. Jest will disable this check for variables that start with the word mock. However, it is still up to you to guarantee that they will be initialized on time. Be aware of Temporal Dead Zone.

For example, the following will throw an out-of-scope error due to the use of fake instead of mock in the variable declaration.

// Note: this will fail
import SoundPlayer from './sound-player';
const fakePlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: fakePlaySoundFile};
});
});

The following will throw a ReferenceError despite using mock in the variable declaration, as the mockSoundPlayer is not wrapped in an arrow function and thus accessed before initialization after hoisting.

import SoundPlayer from './sound-player';
const mockSoundPlayer = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
// results in a ReferenceError
jest.mock('./sound-player', () => {
return mockSoundPlayer;
});

Replacing the mock using mockImplementation() or mockImplementationOnce()

You can replace all of the above mocks in order to change the implementation, for a single test or all tests, by calling mockImplementation() on the existing mock.

Las llamadas a jest.mock son elevadas al principio del código. You can specify a mock later, e.g. in beforeAll(), by calling mockImplementation() (or mockImplementationOnce()) on the existing mock instead of using the factory parameter. Esto también le permite cambiar la simulación entre pruebas, si se necesita:

import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';

jest.mock('./sound-player');

describe('When SoundPlayer throws an error', () => {
beforeAll(() => {
SoundPlayer.mockImplementation(() => {
return {
playSoundFile: () => {
throw new Error('Test error');
},
};
});
});

it('Should throw an error when calling playSomethingCool', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
});
});

A profundidad: Entendiendo las funciones constructor mock

Building your constructor function mock using jest.fn().mockImplementation() makes mocks appear more complicated than they really are. 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

If you define an ES6 class using the same filename as the mocked class in the __mocks__ folder, it will serve as the 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

The module factory function passed to jest.mock(path, moduleFactory) can be a HOF that returns a function*. This will allow calling new on the mock. 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('./sound-player', () => {
return function () {
return {playSoundFile: () => {}};
};
});
note

The mock can't be an arrow function because calling new on an arrow function is not allowed in JavaScript. Así que esto no funciona:

jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions can't be called with new
return {playSoundFile: () => {}};
};
});

This will throw TypeError: _soundPlayer2.default is not a constructor, unless the code is transpiled to ES5, e.g. by @babel/preset-env. (ES5 no tiene funciones de flecha ni clases, por lo que ambas serán transpiladas a funciones simples.)

Mocking a specific method of a class

Lets say that you want to mock or spy on the method playSoundFile within the class SoundPlayer. A simple example:

// your jest test file below
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';

const playSoundFileMock = jest
.spyOn(SoundPlayer.prototype, 'playSoundFile')
.mockImplementation(() => {
console.log('mocked function');
}); // comment this line if just want to "spy"

it('player consumer plays music', () => {
const player = new SoundPlayerConsumer();
player.playSomethingCool();
expect(playSoundFileMock).toHaveBeenCalled();
});

Static, getter and setter methods

Lets imagine our class SoundPlayer has a getter method foo and a static method brand

export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}

playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}

get foo() {
return 'bar';
}
static brand() {
return 'player-brand';
}
}

You can mock/spy on them easily, here is an example:

// your jest test file below
import SoundPlayer from './sound-player';

const staticMethodMock = jest
.spyOn(SoundPlayer, 'brand')
.mockImplementation(() => 'some-mocked-brand');

const getterMethodMock = jest
.spyOn(SoundPlayer.prototype, 'foo', 'get')
.mockImplementation(() => 'some-mocked-result');

it('custom methods are called', () => {
const player = new SoundPlayer();
const foo = player.foo;
const brand = SoundPlayer.brand();

expect(staticMethodMock).toHaveBeenCalled();
expect(getterMethodMock).toHaveBeenCalled();
});

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. Create it with jest.fn(), and then specify its implementation with mockImplementation().

import SoundPlayer from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
});
});

This will let us inspect usage of our mocked class, using SoundPlayer.mock.calls: expect(SoundPlayer).toHaveBeenCalled(); or near-equivalent: expect(SoundPlayer.mock.calls.length).toBeGreaterThan(0);

Creando mocks para exports no default

If the class is not the default export from the module then you need to return an object with the key that is the same as the class export name.

import {SoundPlayer} from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return {
SoundPlayer: jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
}),
};
});

Espiando métodos de nuestra clase

Our mocked class will need to provide any member functions (playSoundFile in the example) that will be called during our tests, or else we'll get an error for calling a function that doesn't exist. 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. To spy on method calls in all of these objects, we populate playSoundFile with another mock function, and store a reference to that same mock function in our test file, so it's available during tests.

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;

Usage is similar to the module factory function, except that you can omit the second argument from jest.mock(), and you must import the mocked method into your test file, since it is no longer defined there. Use the original module path for this; don't include __mocks__.

Limpiando entre pruebas

To clear the record of calls to the mock constructor function and its methods, we call mockClear() in the beforeEach() function:

beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});

Ejemplo completo

Here's a complete test file which uses the module factory parameter to 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]).toBe(coolSoundFileName);
});