Saltar al contenido principal
Versión: 30.0

Simulación de Clases ES6

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Jest puede utilizarse para simular clases ES6 que se importan en archivos que deseas probar.

Las clases ES6 son funciones constructoras con cierto azúcar sintáctico. Por lo tanto, cualquier simulación de una clase ES6 debe ser una función o una clase ES6 real (que, nuevamente, es otra función). Así que puedes simularlas usando funciones simuladas.

Ejemplo de una Clase ES6

Usaremos un ejemplo artificial de una clase que reproduce archivos de sonido, SoundPlayer, y una clase consumidora que utiliza esa clase, SoundPlayerConsumer. Simularemos SoundPlayer en nuestras pruebas para 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 simulación de clase ES6

Simulación automática

Llamar a jest.mock('./sound-player') devuelve una útil "simulación automática" que puedes usar para espiar llamadas al constructor de la clase y todos sus métodos. Reemplaza la clase ES6 con un constructor simulado, y sustituye todos sus métodos con funciones simuladas que siempre devuelven undefined. Las llamadas a métodos se guardan en theAutomaticMock.mock.instances[index].methodName.mock.calls.

nota

Si usas funciones flecha en tus clases, no formarán parte de la simulación. La razón es que las funciones flecha no están presentes en el prototipo del objeto, son meramente 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 de 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);
});

Simulación manual

Crea una simulación manual guardando una implementación simulada en la carpeta __mocks__. Esto te permite especificar la implementación, y puede usarse en múltiples archivos de prueba.

__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 simulado 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);
});

Llamando a jest.mock() con el parámetro de fábrica de módulo

jest.mock(path, moduleFactory) acepta un argumento de fábrica de módulo. Una fábrica de módulo es una función que devuelve la simulación.

Para simular una función constructora, la fábrica de módulo debe devolver una función constructora. Es decir, la fábrica de módulo debe ser una función que devuelva otra función (una función de orden superior - HOF).

import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
precaución

Dado que las llamadas a jest.mock() se elevan al inicio del archivo, Jest impide el acceso a variables fuera de alcance. Por defecto, no puedes definir primero una variable y luego usarla en la fábrica. Jest deshabilitará esta restricción para variables que comiencen con la palabra mock. Sin embargo, sigue siendo tu responsabilidad garantizar que se inicialicen a tiempo. Ten en cuenta la Zona Muerta Temporal.

Por ejemplo, lo siguiente lanzará un error por fuera de alcance debido al uso de fake en lugar de mock en la declaración de variable.

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

Lo siguiente lanzará un ReferenceError a pesar de usar mock en la declaración, ya que mockSoundPlayer no está envuelto en una función flecha y se accede antes de inicializarse tras el ascenso.

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

Reemplazando la simulación con mockImplementation() o mockImplementationOnce()

Puedes reemplazar todas las simulaciones anteriores para cambiar la implementación, ya sea para una sola prueba o todas las pruebas, llamando a mockImplementation() en la simulación existente.

Las llamadas a jest.mock se elevan al inicio del código. Puedes especificar una simulación más tarde, por ejemplo en beforeAll(), llamando a mockImplementation() (o mockImplementationOnce()) en la simulación existente en lugar de usar el parámetro de fábrica. Esto también te permite cambiar la simulación entre pruebas si es necesario:

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

En profundidad: Entendiendo funciones constructoras simuladas

Construir tu función constructora simulada usando jest.fn().mockImplementation() puede hacer que las simulaciones parezcan más complejas de lo que son. Esta sección muestra cómo puedes crear tus propias simulaciones para ilustrar cómo funciona.

Simulación manual que es otra clase ES6

Si defines una clase ES6 con el mismo nombre de archivo que la clase simulada en la carpeta __mocks__, servirá como simulación. Esta clase se usará 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 nuestro ejemplo artificial, la simulació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');
}
}

Simulación usando el parámetro de fábrica de módulo

La función fábrica de módulo pasada a jest.mock(path, moduleFactory) puede ser una HOF que devuelva una función*. Esto permitirá llamar a new en la simulación. Nuevamente, esto te permite inyectar comportamientos diferentes para pruebas, pero no proporciona forma de espiar llamadas.

* La función fábrica de módulo debe devolver una función

Para simular una función constructora, la fábrica de módulo debe devolver una función constructora. Es decir, la fábrica de módulo debe ser una función que devuelva otra función (una función de orden superior - HOF).

jest.mock('./sound-player', () => {
return function () {
return {playSoundFile: () => {}};
};
});
nota

La simulación no puede ser una función flecha porque llamar a new sobre una función flecha no está permitido en JavaScript. Esto no funcionará:

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

Esto lanzará TypeError: _soundPlayer2.default is not a constructor, a menos que el código sea transpilado a ES5, por ejemplo con @babel/preset-env. (ES5 no tiene funciones flecha ni clases, ambas serán transpiladas a funciones simples).

Simulando un método específico de una clase

Supongamos que deseas simular o espiar el método playSoundFile dentro de la clase SoundPlayer. Un ejemplo simple:

// 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();
});

Métodos estáticos, getters y setters

Imaginemos que nuestra clase SoundPlayer tiene un método getter foo y un método estático 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';
}
}

Puedes simularlos/espíarlos fácilmente, aquí un ejemplo:

// 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();
});

Rastreando el uso (espiando la simulación)

Inyectar una implementación de prueba es útil, pero probablemente también quieras probar si el constructor y los métodos de la clase se llaman con los parámetros correctos.

Espiando el constructor

Para rastrear llamadas al constructor, reemplaza la función devuelta por la HOF con una función simulada de Jest. Créala 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 simulada usando SoundPlayer.mock.calls: expect(SoundPlayer).toHaveBeenCalled(); o equivalente: expect(SoundPlayer.mock.calls.length).toBeGreaterThan(0);

Simulando exportaciones de clase no predeterminadas

Si la clase no es la exportación predeterminada del módulo, debes devolver un objeto con una clave igual al nombre de exportación de la clase.

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

Nuestra clase simulada necesitará proporcionar todas las funciones miembro (playSoundFile en el ejemplo) que se llamarán durante nuestras pruebas; de lo contrario, obtendremos un error por llamar a una función que no existe. Pero probablemente también querremos espiar las llamadas a estos métodos para verificar que se invocaron con los parámetros esperados.

Se creará un nuevo objeto cada vez que se llame a la función constructora simulada durante las pruebas. Para espiar las llamadas a métodos en todos estos objetos, asignamos a playSoundFile otra función simulada y guardamos una referencia a esa misma función simulada en nuestro archivo de pruebas, para que esté disponible durante los 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 en un mock manual 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 de módulos, excepto que puedes omitir el segundo argumento de jest.mock(), y debes importar el método simulado en tu archivo de pruebas, ya que allí ya no está definido. Usa la ruta del módulo original para esto; no incluyas __mocks__.

Limpieza entre pruebas

Para borrar el registro de llamadas a la función constructora simulada y sus métodos, llamamos a mockClear() en la función beforeEach():

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

Ejemplo completo

Aquí tienes un archivo de prueba completo que usa el parámetro de fábrica de módulos con 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);
});