Skip to main content
Versão: Próximo

Funções de Simulação

Funções de simulação ( mocks em inglês ) permitem que você teste os links entre códigos, apagando a implementação real de uma função, capturando chamadas para a função (e os parâmetros passados nessas chamadas), capturar instâncias do construtor de funções quando instanciado com new, e permitindo configuração em tempo de teste de valores de retorno.

Existem duas maneiras de simular funções: Seja criando uma função simulada para usar no código de teste, ou escrevendo uma simulação manual para sobrescrever uma dependência de modulo.

Usando uma função de simulação

Vamos imaginar que estamos testando uma implementação de uma função forEach, que invoca uma "callback" para cada item em um array fornecido.

forEach.js
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}

Para testar esta função, podemos usar uma função de simulação e inspecionar o estado da simulação para garantir que a "callback" é invocada como esperado.

forEach.test.js
const forEach = require('./forEach');

const mockCallback = jest.fn(x => 42 + x);

test('forEach mock function', () => {
forEach([0, 1], mockCallback);

// The mock function was called twice
expect(mockCallback.mock.calls).toHaveLength(2);

// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);

// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);
});

Propriedade .mock

Todas as funções de simulação (.mock) possuem esta especial propriedade .mock, que é onde os dados sobre como a função a qual foi chamada são mantidos. A propriedade <code>.mock também pode rastrear o valor do this para cada chamada, então é possível inspecionar isso também:

const myMock1 = jest.fn();
const a = new myMock1();
console.log(myMock1.mock.instances);
// > [ <a> ]

const myMock2 = jest.fn();
const b = {};
const bound = myMock2.bind(b);
bound();
console.log(myMock2.mock.contexts);
// > [ <b> ]

As funções de simulação (mock) são muito úteis nos testes permitindo a verificação de como essas funções então sendo chamadas, instanciadas, ou o que retornam:

// The function was called exactly once
expect(someMockFunction.mock.calls).toHaveLength(1);

// The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');

// The second arg of the first call to the function was 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');

// The return value of the first call to the function was 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value');

// The function was called with a certain `this` context: the `element` object.
expect(someMockFunction.mock.contexts[0]).toBe(element);

// This function was instantiated exactly twice
expect(someMockFunction.mock.instances.length).toBe(2);

// The object returned by the first instantiation of this function
// had a `name` property whose value was set to 'test'
expect(someMockFunction.mock.instances[0].name).toBe('test');

// The first argument of the last call to the function was 'test'
expect(someMockFunction.mock.lastCall[0]).toBe('test');

Mock Valores de Retorno

Mock functions can also be used to inject test values into your code during a test:

const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

Mock functions are also very effective in code that uses a functional continuation-passing style. Code written in this style helps avoid the need for complicated stubs that recreate the behavior of the real component they're standing in for, in favor of injecting values directly into the test right before they're used.

const filterTestFn = jest.fn();

// Make the mock return `true` for the first call,
// and `false` for the second call
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);

const result = [11, 12].filter(num => filterTestFn(num));

console.log(result);
// > [11]
console.log(filterTestFn.mock.calls[0][0]); // 11
console.log(filterTestFn.mock.calls[1][0]); // 12

Most real-world examples actually involve getting ahold of a mock function on a dependent component and configuring that, but the technique is the same. In these cases, try to avoid the temptation to implement logic inside of any function that's not directly being tested.

Simulando módulos

Vamos supor que temos uma classe que traz usuários da nossa API. A classe usa axios para chamar a API, e então retorna o atributo data onde contém todos os usuários:

users.js
import axios from 'axios';

class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}

export default Users;

Agora, para testar este método sem consumir a API (e assim, criando testes lentos e frágeis), nós podemos usar a função jest.mock(...) para simular automaticamente o módulo do axios.

Assim que simulamos o módulo, podemos fornecer uma mockResolvedValue para o .get que retorna os dados que queremos que nosso teste afirme contra. Assim, estamos dizendo que queremos que o axios.get('/users.json') retorne uma resposta falsa.

users.test.js
import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('deve buscar os usuários', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);

// ou você poderá usar o código abaixo dependendo do seu caso de uso:
// axios.get.mockImplementation(() => Promise.resolve(resp))

return Users.all().then(data => expect(data).toEqual(users));
});

Mocking Partials

Subsets of a module can be mocked and the rest of the module can keep their actual implementation:

foo-bar-baz.js
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
//test.js
import defaultExport, {bar, foo} from '../foo-bar-baz';

jest.mock('../foo-bar-baz', () => {
const originalModule = jest.requireActual('../foo-bar-baz');

//Mock the default export and named export 'foo'
return {
__esModule: true,
...originalModule,
default: jest.fn(() => 'mocked baz'),
foo: 'mocked foo',
};
});

test('should do a partial mock', () => {
const defaultExportResult = defaultExport();
expect(defaultExportResult).toBe('mocked baz');
expect(defaultExport).toHaveBeenCalled();

expect(foo).toBe('mocked foo');
expect(bar()).toBe('bar');
});

Implementações de Mock

Still, there are cases where it's useful to go beyond the ability to specify return values and full-on replace the implementation of a mock function. This can be done with jest.fn or the mockImplementationOnce method on mock functions.

const myMockFn = jest.fn(cb => cb(null, true));

myMockFn((err, val) => console.log(val));
// > true

The mockImplementation method is useful when you need to define the default implementation of a mock function that is created from another module:

foo.js
module.exports = function () {
// some implementation;
};
test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');

// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42

When you need to recreate a complex behavior of a mock function such that multiple function calls produce different results, use the mockImplementationOnce method:

const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, false));

myMockFn((err, val) => console.log(val));
// > true

myMockFn((err, val) => console.log(val));
// > false

When the mocked function runs out of implementations defined with mockImplementationOnce, it will execute the default implementation set with jest.fn (if it is defined):

const myMockFn = jest
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

For cases where we have methods that are typically chained (and thus always need to return this), we have a sugary API to simplify this in the form of a .mockReturnThis() function that also sits on all mocks:

const myObj = {
myMethod: jest.fn().mockReturnThis(),
};

// is the same as

const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};

Nome das funções de simulação (Mock names)

You can optionally provide a name for your mock functions, which will be displayed instead of 'jest.fn()' in the test error output. Use .mockName() if you want to be able to quickly identify the mock function reporting an error in your test output.

const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(scalar => 42 + scalar)
.mockName('add42');

Matchers personalizados

Finally, in order to make it less demanding to assert how mock functions have been called, we've added some custom matcher functions for you:

// The mock function was called at least once
expect(mockFunc).toHaveBeenCalled();

// The mock function was called at least once with the specified args
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);

// The last call to the mock function was called with the specified args
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);

// All calls and the name of the mock is written as a snapshot
expect(mockFunc).toMatchSnapshot();

These matchers are sugar for common forms of inspecting the .mock property. You can always do this manually yourself if that's more to your taste or if you need to do something more specific:

// The mock function was called at least once
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);

// The mock function was called at least once with the specified args
expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);

// The last call to the mock function was called with the specified args
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
arg1,
arg2,
]);

// The first arg of the last call to the mock function was `42`
// (note that there is no sugar helper for this specific of an assertion)
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);

// A snapshot will check that a mock was invoked the same number of times,
// in the same order, with the same arguments. Ele também irá testar o nome da função de simulação.
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.getMockName()).toBe('a mock name');

Para obter uma lista completa de "matchers", confira a documentação de referência.