Mock Functions
Las funciones de simulación mock permiten probar los vínculos entre código deshaciéndose de la implementación real de una función, capturando llamadas a la función (y los parámetros de dicha llamada), capturando instancias de funciones de construcción cuando estas se crean instancias con new
, y permitiendo configuración de valores de retorno al momento de probar.
Existen dos formas de simular funciones con mocks: Ya sea creando una función mock para usar en la prueba, o escribiendo un mock manual
para sobrescribir la dependencia a un módulo.
Usando una función mock #
Supongamos se esta probando la implementación de una función forEach
que invoca una función callback para cada objeto del arreglo que se provee.
function forEach(items, callback) { for (let index = 0; index < items.length; index++) { callback(items[index]); } }
Para probar esta función, podemos ocupar una función mock, e inspeccionar el estado del mock para asegurarnos que se llama a la función callback como es esperado.
const mockCallback = jest.fn(); forEach([0, 1], mockCallback); // The mock function is called twice expect(mockCallback.mock.calls.length).toBe(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);
propiedad .mock
#
Todas las funciones simuladas mock tienen la propiedad .mock
, donde se guarda la información sobre como se ha llamado a esa función. La propiedad .mock
guarda también el valor de this
correspondiente a cada llamada, de modo que también se puede inspeccionar lo siguiente:
const miMock = jest.fn(); const a = new miMock(); const b = {}; const bound = miMock.bind(b); bound(); console.log(miMock.mock.instances); // > [ <a>, <b> ]
Estos miembros de la simulación mock son extremadamente útiles en pruebas para identificar como es que estas funciones fueron ejecutadas o como se crearon sus instancias:
// La función se ejecuto una sola vez expect(unaFuncionMock.mock.calls.length).toBe(1); // El primer argumento de la primera llamada fue 'first arg' expect(unaFuncionMock.mock.calls[0][0]).toBe('first arg'); // El segundo argumento de la primera llamada fue 'second arg' expect(unaFuncionMock.mock.calls[0][1]).toBe('second arg'); // Se crearon exactamente dos instancias de la función expect(unaFuncionMock.mock.instances.length).toBe(2); // El primer objeto regresado de la primera instancia de esta función // tenia una propiedad `name` con valor 'test' expect(unaFuncionMock.mock.instances[0].name).toEqual('test');
Simular valores de retorno #
Las funciones simuladas mock pueden ser utilizadas para inyectar valores de prueba en el código de pruebas:
const miMock = jest.fn(); console.log(miMock()); // > undefined miMock.mockReturnValueOnce(10) .mockReturnValueOnce('x') .mockReturnValue(true); console.log(miMock(), miMock(), miMock(), miMock()); // > 10, 'x', true, true
Las funciones mock son también efectivas en código que ocupa un estilo funcional de llamadas continuas. Código escrito en este estilo evita la complejidad de crear módulos simulados que recreen la información del componente real que están sustituyendo, a favor de inyectar los valores en la prueba directamente donde van a ser utilizados.
const pruebaFnFiltro = jest.fn(); // Establece que el mock regresa `true` en la primera llamada, // y `false` en la segunda pruebaFnFiltro .mockReturnValueOnce(true) .mockReturnValueOnce(false); const resultado = [11, 12].filter(pruebaFnFiltro); console.log(resultado); // > [11] console.log(pruebaFnFiltro.mock.calls); // > [ [11], [12] ]
La mayoría de ejemplos de código real involucran el conseguir una función mock de un componente dependencia y configurar ésta, pero la técnica aplicada es la misma. En esos casos, se debe evitar el implementar lógica dentro de cualquier función que no se este probando.
Implementaciones de simulaciones mock #
Sin embargo, existen casos donde es útil no solo reemplazar los valores de retorno de una función, sino reemplazar la función por completo. Esto se puede realizar con jest.fn
o el método mockImplementationOnce
en funciones mock.
const myMockFn = jest.fn(cb => cb(null, true)); myMockFn((err, val) => console.log(val)); // > true myMockFn((err, val) => console.log(val)); // > true
El método mockImplementation
es útil cuando se desea definir el comportamiento por defecto de una función mock que es creada en otro módulo:
// 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
Cuando se requiere recrear comportamiento complejo de modo que diferentes llamadas una función simulada mock regresen diferentes resultados, se puede usar el 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
Cuando la función simulada mock haya agotado las implementaciones definidas en mockImplementationOnce
, está ejecutará la implementación por defecto definida en jest.fn
(sí es que se definió):
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 donde haya métodos ejecutados en secuencia (y por lo tanto donde siempre se requiera regresar this
), existe un API que simplifica de manera agradable esta situación, a través del método .mockReturnThis()
que se encuentra en todos los mocks:
const myObj = { myMethod: jest.fn().mockReturnThis(), }; // is the same as const otherObj = { myMethod: jest.fn(function() { return this; }), };
Matchers comunes #
Finalmente, para simplificar el proceso de identificar como se llamaron las funciones simuladas mock, existen funciones de comparación personalizadas:
// The mock function was called at least once expect(mockFunc).toBeCalled(); // The mock function was called at least once with the specified args expect(mockFunc).toBeCalledWith(arg1, arg2); // The last call to the mock function was called with the specified args expect(mockFunc).lastCalledWith(arg1, arg2);
Estos métodos de comparacion, o matchers, son formas sencillas de inspeccionar la propiedad .mock
. Siempre se pueden realizar las comparaciones manualmente si se desea hacer algo más especifico:
// 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).toContain([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);
Para una lista completa de matchers, véase los documentos de referencia reference docs.