Mock Functions
Funções Mock, ou de simulação, facilitam testar as ligações entre código por apagar a implementação real de uma função, capturando chamadas para a função (e os parâmetros passados nessas chamadas), capturando instâncias de funções construtoras quando instanciadas com new
e que permite a configuração test-time dos valores de retorno.
Há duas maneiras para simular (mock, em inglês) funções: ou através da criação de uma função de simulação para usar em código de teste. ou escrevendo uma simulação manual
para sobrepor uma dependência de módulo.
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.
function forEach(items, callback) { for (let index = 0; index < items.length; index++) { callback(items[index]); } }
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.
const mockCallback = jest.fn(); forEach([0, 1], mockCallback); // A função de simulação é chamada duas vezes expect(mockCallback.mock.calls.length).toBe(2); // O primeiro argumento da primeira chamada para a função foi 0 expect(mockCallback.mock.calls[0][0]).toBe(0); // O primeiro argumento da segunda chamada para a função foi 1 expect(mockCallback.mock.calls[1][0]).toBe(1);
Propriedade .mock
#
Todas as funções de simulação (mock, em inglês) têm esta propriedade especial .mock
, que é onde os dados sobre como a função foi chamada são mantidos. A propriedade .mock
também controla o valor this
para cada chamada, portanto, é possível inspecionar "this" também:
const myMock = jest.fn(); const a = new myMock(); const b = {}; const bound = myMock.bind(b); bound(); console.log(myMock.mock.instances); // > [ <a>, <b> ]
Esses membros simulados (mock, em inglês) são muito úteis em testes para afirmar como essas funções são chamadas, ou instanciadas:
// A função foi chamada exatamente uma vez expect(someMockFunction.mock.calls.length).toBe(1); // O primeiro argumento da primeira chamada foi 'first arg' expect(someMockFunction.mock.calls[0][0]).toBe('first arg'); // O segundo argumento da segunda chamada foi 'second arg' expect(someMockFunction.mock.calls[0][1]).toBe('second arg'); // Esta função foi instanciada exatamente duas vezes expect(someMockFunction.mock.instances.length).toBe(2); // O objeto retornado pela primeira instanciação dessa função // tem uma propriedade `name` na qual o valor foi setado para 'test' expect(someMockFunction.mock.instances[0].name).toEqual('test');
Mock Valores de Retorno #
Funções de simulação (mock, em inglês) também podem ser usadas para injetar valores de teste em seu código durante um teste:
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
Funções de simulação (mock, em inglês) também são muito eficazes em código que usa um estilo funcional de "continuation-passing". Código escrito neste estilo ajuda a evitar a necessidade de esboços complicados que recriam o comportamento do componente real que eles representam, em favor de injetar valores diretamente no teste logo antes que eles são usados.
const filterTestFn = jest.fn(); // Faz a simulação retornar `true` para a primeira chamada, // e `false` para a segunda chamada filterTestFn .mockReturnValueOnce(true) .mockReturnValueOnce(false); const result = [11, 12].filter(filterTestFn); console.log(result); // > [11] console.log(filterTestFn.mock.calls); // > [ [11], [12] ]
A maioria dos exemplos do mundo real envolvem a obtenção de uma função simulada (mock, em inglês) em um componente dependente e configurar isso, mas a técnica é a mesma. Nestes casos, tente evitar a tentação de implementar lógica dentro de qualquer função que não está sendo testada diretamente.
Implementações de Mock #
Ainda assim, existem casos em que é útil ir além da capacidade de especificar os valores de retorno e substituir completamente a implementação de uma função de simulação (mock, em inglês). Isto pode ser feito com jest.fn
ou o método mockImplementationOnce
em funções simuladas (mock, em inglês).
const myMockFn = jest.fn(cb => cb(null, true)); myMockFn((err, val) => console.log(val)); // > true myMockFn((err, val) => console.log(val)); // > true
O método mockImplementation
é útil quando você precisa definir a implementação padrão de uma função de simulação (mock, em inglês) que é criada a partir de outro módulo:
// foo.js module.exports = function() { // some implementation; }; // test.js jest.mock('../foo'); // isso acontece automaticamente com automocking const foo = require('../foo'); // foo é uma função simulada, ou mock function foo.mockImplementation(() => 42); foo(); // > 42
Quando você precisar recriar um comportamento complexo de uma função de simulação (mock, em inglês) que várias chamadas de função produzem resultados diferentes, use o método mockImplementationOnce
:
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
Quando a função simulada (mocked, em inglês) fica sem implementações definidas com mockImplementationOnce
, ele irá executar a conjunto de implementação padrão com jest.fn
(se estiver definido):
const myMockFn = jest.fn(() => 'default') .mockImplementationOnce(() => 'first call') .mockImplementationOnce(() => 'second call'); console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn()); // > 'first call', 'second call', 'default', 'default'
Para casos onde temos métodos que normalmente são encadeados (e, portanto, sempre precisam retornar this
), temos uma API para simplificar isto sob a forma de uma função .mockReturnThis()
que também se posiciona em todas as simulações:
const myObj = { myMethod: jest.fn().mockReturnThis(), }; // é o mesmo que const otherObj = { myMethod: jest.fn(function() { return this; }), };
Matchers personalizados #
Finalmente, para tornar mais simples a afirmação de como as funções simuladas (mock, em inglês) foram chamadas, nós adicionamos algumas funções de correspondência (matcher, em inglês) personalizadas para você:
// A função simulada foi chamada pelo menos uma vez expect(mockFunc).toBeCalled(); // A função simulada foi chamada pelo menos uma vez com os args especificados expect(mockFunc).toBeCalledWith(arg1, arg2); // A última chamada para a função simulada foi chamada com os args especificados expect(mockFunc).lastCalledWith(arg1, arg2);
Estas correspondências (matchers, em inglês) são realmente muito simples para formas comuns de inspecionar a propriedade .mock
. Você sempre pode fazer isso manualmente você mesmo se é mais do seu gosto, ou se você precisa fazer algo mais específico:
// A função simulada foi chamada pelo menos uma vez expect(mockFunc.mock.calls.length).toBeGreaterThan(0); // A função simulada foi chamada pelo menos uma vez com os args especificados expect(mockFunc.mock.calls).toContain([arg1, arg2]); // A última chamada para a função simulada foi chamada com os args especificados expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual( [arg1, arg2] ); // O primeiro arg da última chamada para a função simulada foi `42` // (note que não há nenhum auxiliar para essa assertiva específica) expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);
Para obter uma lista completa de "matchers", confira os documentos de referência.