¿Cómo puedo burlarme de una importación de módulo ES6 usando Jest?

281

Estoy empezando a pensar que esto no es posible, pero quiero preguntar de todos modos.

Quiero probar que uno de mis módulos ES6 llama a otro módulo ES6 de una manera particular. Con Jasmine esto es súper fácil.

El código de la aplicación:

// myModule.js
import dependency from './dependency';

export default (x) => {
  dependency.doSomething(x * 2);
}

Y el código de prueba:

//myModule-test.js
import myModule from '../myModule';
import dependency from '../dependency';

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    spyOn(dependency, 'doSomething');

    myModule(2);

    expect(dependency.doSomething).toHaveBeenCalledWith(4);
  });
});

¿Cuál es el equivalente con Jest? Siento que esto es algo tan simple que quiero hacer, pero me he estado arrancando el pelo tratando de resolverlo.

Lo más cerca que he estado es reemplazando imports con requires, y moviéndolos dentro de las pruebas / funciones. Ninguna de las cuales son cosas que quiero hacer.

// myModule.js
export default (x) => {
  const dependency = require('./dependency'); // yuck
  dependency.doSomething(x * 2);
}

//myModule-test.js
describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    jest.mock('../dependency');

    myModule(2);

    const dependency = require('../dependency'); // also yuck
    expect(dependency.doSomething).toBeCalledWith(4);
  });
});

Para obtener puntos de bonificación, me encantaría hacer que todo funcione cuando la función dentro dependency.jses una exportación predeterminada. Sin embargo, sé que espiar las exportaciones predeterminadas no funciona en Jasmine (o al menos nunca podría hacerlo funcionar), por lo que tampoco tengo la esperanza de que sea posible en Jest.

Cam Jackson
fuente
De todos modos, estoy usando Babel para este proyecto, así que no me importa continuar transpilando imports a requires por ahora. Gracias por el aviso.
Cam Jackson
¿Y si tengo ts clase A y llama alguna función permite decir doSomething () de la clase B, ¿cómo podemos burlarse de modo que la clase A hace la llamada a la versión burlado de la función de la clase B doSomething ()
kailash Yogeshwar
para aquellos que quieran descubrir este problema más github.com/facebook/jest/issues/936
omeralper

Respuestas:

221

He podido resolver esto usando un truco que involucra import *. ¡Incluso funciona para exportaciones con nombre y predeterminadas!

Para una exportación con nombre:

// dependency.js
export const doSomething = (y) => console.log(y)

// myModule.js
import { doSomething } from './dependency';

export default (x) => {
  doSomething(x * 2);
}

// myModule-test.js
import myModule from '../myModule';
import * as dependency from '../dependency';

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    dependency.doSomething = jest.fn(); // Mutate the named export

    myModule(2);

    expect(dependency.doSomething).toBeCalledWith(4);
  });
});

O para una exportación predeterminada:

// dependency.js
export default (y) => console.log(y)

// myModule.js
import dependency from './dependency'; // Note lack of curlies

export default (x) => {
  dependency(x * 2);
}

// myModule-test.js
import myModule from '../myModule';
import * as dependency from '../dependency';

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    dependency.default = jest.fn(); // Mutate the default export

    myModule(2);

    expect(dependency.default).toBeCalledWith(4); // Assert against the default
  });
});

Como Mihai Damian señaló con razón a continuación, esto está mutando el objeto del módulo dependencyy, por lo tanto, se 'filtrará' a otras pruebas. Entonces, si usa este enfoque, debe almacenar el valor original y luego volverlo a configurar después de cada prueba. Para hacer esto fácilmente con Jest, use el método spyOn () en lugar de jest.fn()porque admite restaurar fácilmente su valor original, evitando así la 'fuga' antes mencionada.

Cam Jackson
fuente
Gracias por compartir. Creo que el resultado neto es similar a esto - pero esto podría ser más limpio - stackoverflow.com/a/38414160/1882064
arcseldon
64
Esto funciona, pero probablemente no sea una buena práctica. Los cambios en los objetos fuera del alcance de la prueba parecen persistir entre las pruebas. Más tarde, esto puede conducir a resultados inesperados en otras pruebas.
Mihai Damian
10
En lugar de usar jest.fn (), puede usar jest.spyOn () para poder restaurar el método original más adelante, para que no se filtre en otras pruebas. Encontré un buen artículo sobre diferentes enfoques aquí (jest.fn, jest.mock y jest.spyOn): medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c .
Martinsos
2
Solo una nota: si dependencyreside en el mismo archivo que myModule, no funcionará.
Lu Tran
3
Creo que esto no funcionará con Typecript, el objeto que está mutando es de solo lectura.
Adredx
172

Tienes que burlarte del módulo y configurar el espía tú mismo:

import myModule from '../myModule';
import dependency from '../dependency';
jest.mock('../dependency', () => ({
  doSomething: jest.fn()
}))

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    myModule(2);
    expect(dependency.doSomething).toBeCalledWith(4);
  });
});
Andreas Köberle
fuente
44
Esto no parece correcto. Entiendo: babel-plugin-jest-hoist: The second argument of jest.mock must be a function.el código ni siquiera se está compilando.
Cam Jackson
3
Lo siento, he actualizado mi código. Tenga en cuenta también que la ruta de acceso jest.mockes relativa al archivo de prueba.
Andreas Köberle el
1
Sin embargo, esto funcionó para mí, no cuando se utilizan exportaciones predeterminadas.
Iris Schaffer
44
@IrisSchaffer para que esto funcione con la exportación predeterminada que necesita agregar __esModule: trueal objeto simulado. Esa es la bandera interna utilizada por el código transpilado para determinar si se trata de un módulo es6 transpilado o un módulo commonjs.
Johannes Lumpe
24
jest.mock('../dependency', () => ({ default: jest.fn() }))
Burlarse de las
50

Para burlarse de una exportación predeterminada del módulo de dependencia ES6 usando jest:

import myModule from '../myModule';
import dependency from '../dependency';

jest.mock('../dependency');

// If necessary, you can place a mock implementation like this:
dependency.mockImplementation(() => 42);

describe('myModule', () => {
  it('calls the dependency once with double the input', () => {
    myModule(2);

    expect(dependency).toHaveBeenCalledTimes(1);
    expect(dependency).toHaveBeenCalledWith(4);
  });
});

Las otras opciones no funcionaron para mi caso.

falsarella
fuente
66
¿Cuál es la mejor manera de limpiar esto si solo quiero hacer una prueba? adentro después de cada `` `` afterEach (() => {jest.unmock (../ dependency ');}) `` ``
nxmohamad
1
@falsarella ¿funciona doMock en ese caso? Tengo un problema muy similar y no hace nada cuando intento jest.doMock dentro de una prueba específica, donde jest.mock para todo el módulo funciona correctamente
Progress1ve
1
@ Progress1ve también puedes intentar usar jest.mock con mockImplementationOnce
falsarella
1
Sí, esa es una sugerencia válida, sin embargo, eso requiere que la prueba sea la primera y no soy fanático de escribir pruebas de esa manera. Resolví esos problemas importando un módulo externo y usando spyOn en funciones específicas.
Progress1ve
1
@ Progress1ve hmm Quería colocar el simulacro de implementación una vez dentro de cada prueba específica ... de todos modos, estoy feliz de que hayas encontrado una solución :)
falsarella
38

Añadiendo más a la respuesta de Andreas. Tuve el mismo problema con el código ES6 pero no quise mutar las importaciones. Eso parecía hacky. Entonces hice esto

import myModule from '../myModule';
import dependency from '../dependency';
jest.mock('../dependency');

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    myModule(2);
  });
});

Y agregó dependency.js en la carpeta "__ mocks __" paralela a dependency.js. Esto funcionó para mí. Además, esto me dio la opción de devolver datos adecuados de la implementación simulada. Asegúrese de dar la ruta correcta al módulo que desea simular.

mdsAyubi
fuente
Gracias por esto. Le daría una oportunidad. También me gustó esta solución - stackoverflow.com/a/38414160/1882064
arcseldon el
Lo que me gusta de este enfoque es que le brinda la posibilidad de proporcionar un simulacro manual para todas las ocasiones en que desee simular un módulo específico. Por ejemplo, tengo un asistente de traducción, que se usa en muchos lugares. El __mocks__/translations.jsarchivo simplemente exporta por defecto jest.fn()en algo como:export default jest.fn((id) => id)
Iris Schaffer
También puede usar jest.genMockFromModulepara generar simulacros a partir de módulos. facebook.github.io/jest/docs/…
Varunkumar Nagarajan
2
Una cosa a tener en cuenta es que los módulos ES6 burlados export default jest.genMockFromModule('../dependency')tendrán todas sus funciones asignadas dependency.defaultdespués de llamar a `jest.mock ('... dependencia'), pero de lo contrario se comportarán como se espera.
jhk
77
¿Cómo se ve su afirmación de prueba? Eso parece una parte importante de la respuesta. expect(???)
piedra
14

Avance rápido hasta 2020, este enlace me pareció la solución. utilizando solo la sintaxis del módulo ES6 https://remarkablemark.org/blog/2018/06/28/jest-mock-default-named-export/

// esModule.js
export default 'defaultExport';
export const namedExport = () => {};

// esModule.test.js
jest.mock('./esModule', () => ({
  __esModule: true, // this property makes it work
  default: 'mockedDefaultExport',
  namedExport: jest.fn(),
}));

import defaultExport, { namedExport } from './esModule';
defaultExport; // 'mockedDefaultExport'
namedExport; // mock function

También una cosa que necesita saber (que me llevó un tiempo descubrir) es que no puede llamar a jest.mock () dentro de la prueba; debes llamarlo en el nivel superior del módulo. Sin embargo, puede llamar a mockImplementation () dentro de pruebas individuales si desea configurar diferentes simulacros para diferentes pruebas.

Andy
fuente
5

La pregunta ya está respondida, pero puede resolverla así:

dependency.js

const doSomething = (x) => x
export default doSomething;

myModule.js:

import doSomething from "./dependency";

export default (x) => doSomething(x * 2);

myModule.spec.js:

jest.mock('../dependency');
import doSomething from "../dependency";
import myModule from "../myModule";

describe('myModule', () => {
  it('calls the dependency with double the input', () => {
    doSomething.mockImplementation((x) => x * 10)

    myModule(2);

    expect(doSomething).toHaveBeenCalledWith(4);
    console.log(myModule(2)) // 40
  });
});
Delgado
fuente
Pero "requerir" es la sintaxis de CommonJS - OP preguntaba por los módulos ES6
Andy
@ Andy, gracias por tu comentario, actualicé mi respuesta. Por cierto lo mismo en la lógica.
Slim
2

Resolví esto de otra manera. Digamos que tienes tu dependencia.js

export const myFunction = () => { }

Creo un archivo depdency.mock.js junto con el siguiente contenido:

export const mockFunction = jest.fn();

jest.mock('dependency.js', () => ({ myFunction: mockFunction }));

y en la prueba, antes de importar el archivo que tiene la dependencia que uso:

import { mockFunction } from 'dependency.mock'
import functionThatCallsDep from './tested-code'

it('my test', () => {
    mockFunction.returnValue(false);

    functionThatCallsDep();

    expect(mockFunction).toHaveBeenCalled();

})
Felipe Leusin
fuente