Ir para o conteúdo principal
Versão: 30.0

Mocks de Classes ES6

Tradução Beta Não Oficial

Esta página foi traduzida por PageTurner AI (beta). Não é oficialmente endossada pelo projeto. Encontrou um erro? Reportar problema →

O Jest pode ser usado para simular classes ES6 importadas em arquivos que você deseja testar.

Classes ES6 são funções construtoras com açúcar sintático. Portanto, qualquer simulação para uma classe ES6 deve ser uma função ou uma classe ES6 real (que também é outra função). Assim, você pode simulá-las usando funções mock.

Exemplo de uma Classe ES6

Usaremos um exemplo artificial de uma classe que reproduz arquivos de som, SoundPlayer, e uma classe consumidora que utiliza essa classe, SoundPlayerConsumer. Simularemos o SoundPlayer em nossos testes para o 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);
}
}

As 4 formas de criar um mock para classe ES6

Mock automático

Chamar jest.mock('./sound-player') retorna um "mock automático" útil para monitorar chamadas ao construtor da classe e todos os seus métodos. Ele substitui a classe ES6 por um construtor mock e todos os seus métodos por funções mock que sempre retornam undefined. As chamadas de métodos são salvas em theAutomaticMock.mock.instances[index].methodName.mock.calls.

"message": "nota"

Se você usar arrow functions em suas classes, elas não farão parte do mock. Isso ocorre porque arrow functions não estão presentes no protótipo do objeto, sendo apenas propriedades que referenciam uma função.

Se você não precisa substituir a implementação da classe, esta é a opção mais fácil de configurar. Por exemplo:

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

Crie um mock manual salvando uma implementação simulada na pasta __mocks__. Isso permite especificar a implementação e reutilizá-la em vários arquivos de teste.

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

Importe o mock e o método mock compartilhado por todas as instâncias:

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

Chamando jest.mock() com o parâmetro de fábrica de módulo

jest.mock(path, moduleFactory) recebe um argumento de fábrica de módulo. Uma fábrica de módulo é uma função que retorna o mock.

Para mockar uma função construtora, a fábrica do módulo deve retornar uma função construtora. Em outras palavras, a fábrica do módulo deve ser uma função que retorna outra função - uma função de alta ordem (HOF).

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

Como as chamadas jest.mock() são içadas para o topo do arquivo, o Jest impede o acesso a variáveis fora do escopo. Por padrão, você não pode definir uma variável e depois usá-la na fábrica. O Jest desativa essa verificação para variáveis que começam com mock. Contudo, você ainda é responsável por garantir que elas serão inicializadas a tempo. Esteja ciente da Zona Temporalmente Morta.

Por exemplo, o seguinte lançará um erro de escopo devido ao uso de fake em vez de mock na declaração da variável.

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

O seguinte lançará um ReferenceError apesar de usar mock na declaração, pois mockSoundPlayer não está encapsulado em uma arrow function e é acessado antes da inicialização após o içamento.

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

Substituindo o mock usando mockImplementation() ou mockImplementationOnce()

Você pode substituir todos os mocks acima para alterar a implementação, em um único teste ou em todos os testes, chamando mockImplementation() no mock existente.

As chamadas para jest.mock são içadas (hoisted) para o topo do código. Você pode especificar um mock posteriormente, por exemplo em beforeAll(), chamando mockImplementation() (ou mockImplementationOnce()) no mock existente em vez de usar o parâmetro de fábrica. Isso também permite alterar o mock entre testes, se necessário:

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

Detalhes: Entendendo funções construtoras mockadas

Construir sua função construtora mockada usando jest.fn().mockImplementation() pode fazer os mocks parecerem mais complexos do que realmente são. Esta seção mostra como criar seus próprios mocks para ilustrar como o mock funciona.

Mock manual como outra classe ES6

Se você definir uma classe ES6 com o mesmo nome de arquivo da classe mockada na pasta __mocks__, ela servirá como mock. Esta classe será usada no lugar da classe real. Isso permite injetar uma implementação de teste para a classe, mas não fornece uma maneira de espionar chamadas.

Para o exemplo simplificado, o mock poderia ser assim:

__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 usando o parâmetro de fábrica do módulo

A função de fábrica do módulo passada para jest.mock(path, moduleFactory) pode ser uma função de alta ordem (HOF) que retorna uma função*. Isso permitirá chamar new no mock. Novamente, isso permite injetar comportamentos diferentes para teste, mas não fornece uma maneira de espionar chamadas.

* A função de fábrica do módulo deve retornar uma função

Para mockar uma função construtora, a fábrica do módulo deve retornar uma função construtora. Em outras palavras, a fábrica do módulo deve ser uma função que retorna outra função - uma função de alta ordem (HOF).

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

O mock não pode ser uma arrow function porque chamar new em arrow functions não é permitido em JavaScript. Portanto, isso não funcionará:

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

Isso lançará TypeError: _soundPlayer2.default is not a constructor, a menos que o código seja transpilado para ES5 (ex: com @babel/preset-env). (ES5 não tem arrow functions nem classes, então ambos seriam transpilados para funções simples.)

Mockando um método específico de uma classe

Suponha que você queira mockar ou espionar o método playSoundFile dentro da classe SoundPlayer. Exemplo simples:

// 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 e setters

Imagine que nossa classe SoundPlayer tenha um getter foo e um 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';
}
}

Você pode mockar/espioná-los facilmente, veja um exemplo:

// 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 uso (espionando o mock)

Injetar uma implementação de teste é útil, mas você provavelmente também vai querer testar se o construtor e os métodos da classe são chamados com os parâmetros corretos.

Espionando o construtor

Para rastrear chamadas ao construtor, substitua a função retornada pela HOF por uma função mock do Jest. Crie-a com jest.fn() e depois especifique sua implementação com mockImplementation().

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

Isso nos permite inspecionar o uso da classe mockada através de SoundPlayer.mock.calls: expect(SoundPlayer).toHaveBeenCalled(); ou equivalente: expect(SoundPlayer.mock.calls.length).toBeGreaterThan(0);

Mockando exportações não-padrão de classes

Se a classe não for uma exportação padrão do módulo, você precisa retornar um objeto com a chave igual ao nome da exportação da classe.

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

Espionando métodos da nossa classe

Nossa classe simulada precisará fornecer quaisquer funções de membro (playSoundFile no exemplo) que serão chamadas durante nossos testes, caso contrário receberemos um erro por chamar uma função que não existe. Mas provavelmente também queremos monitorar as chamadas desses métodos para garantir que foram executados com os parâmetros esperados.

Um novo objeto será criado sempre que a função construtora simulada for chamada durante os testes. Para monitorar chamadas de métodos em todos esses objetos, preenchemos playSoundFile com outra função simulada e armazenamos uma referência a essa mesma função simulada em nosso arquivo de teste, para que esteja disponível durante os testes.

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

O equivalente de simulação manual seria:

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

O uso é similar à função de fábrica de módulos, exceto que você pode omitir o segundo argumento de jest.mock(), e deve importar o método simulado para seu arquivo de teste, pois ele não está mais definido lá. Use o caminho do módulo original para isso; não inclua __mocks__.

Limpeza entre testes

Para limpar o registro de chamadas da função construtora simulada e seus métodos, chamamos mockClear() na função beforeEach():

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

Exemplo completo

Aqui está um arquivo de teste completo usando o parâmetro de fábrica de módulos com 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);
});