Como posso simular uma importação de módulo ES6 usando o Jest?

281

Estou começando a pensar que isso não é possível, mas quero perguntar de qualquer maneira.

Quero testar se um dos meus módulos ES6 chama outro módulo ES6 de uma maneira específica. Com Jasmine, isso é super fácil -

O código do aplicativo:

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

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

E o código de teste:

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

Qual é o equivalente com Jest? Eu sinto que isso é uma coisa tão simples de se querer fazer, mas eu tenho arrancado meu cabelo tentando descobrir isso.

O mais próximo que cheguei é substituindo os importpor requires e movendo-os para dentro dos testes / funções. Nenhuma das coisas que eu quero fazer.

// 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 pontos de bônus, eu adoraria fazer a coisa toda funcionar quando a função interna dependency.jsfor uma exportação padrão. No entanto, eu sei que espionar as exportações padrão não funciona no Jasmine (ou pelo menos eu nunca consegui fazê-lo funcionar), então não tenho esperança de que seja possível no Jest também.

Cam Jackson
fonte
De qualquer maneira, estou usando o Babel para este projeto, por isso não me importo de continuar a transpilar imports para requires por enquanto. Obrigado pelo alerta.
cam
E se eu tiver ts classe A e chama alguma função permite dizer doSomething () da classe B como podemos zombar para que a classe A faz chamada para a versão zombou da função de classe B doSomething ()
Kailash Yogeshwar
para quem quiser descobrir esse problema mais github.com/facebook/jest/issues/936
omeralper 12/02/19

Respostas:

219

Consegui resolver isso usando um hack envolvendo import *. Até funciona para exportações nomeadas e padrão!

Para uma exportação nomeada:

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

Ou para uma exportação padrão:

// 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 apontou corretamente, isso está mudando o objeto do módulo dependencye, portanto, 'vazará' para outros testes. Portanto, se você usar essa abordagem, armazene o valor original e volte a configurá-lo após cada teste. Para fazer isso facilmente com o Jest, use o método spyOn () em vez de jest.fn()porque ele suporta facilmente a restauração de seu valor original, evitando assim o vazamento mencionado anteriormente.

Cam Jackson
fonte
Obrigado por compartilhar. Eu acho que o resultado líquido é semelhante a isso - mas isso pode ser mais limpo - stackoverflow.com/a/38414160/1882064
arcseldon
64
Isso funciona, mas provavelmente não é uma boa prática. Alterações em objetos fora do escopo do teste parecem persistir entre os testes. Mais tarde, isso pode levar a resultados inesperados em outros testes.
Mihai Damian
10
Em vez de usar o jest.fn (), você pode usar o jest.spyOn () para restaurar o método original posteriormente, para que ele não vaze em outros testes. Encontrei um bom artigo sobre diferentes abordagens aqui (jest.fn, jest.mock e jest.spyOn): medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c .
Martinsos
2
Apenas uma observação: se o dependencyarquivo residir no mesmo arquivo myModule, ele não funcionará.
11788 Lu Lu
2
Eu acho que isso não funcionará com o Typecript, o objeto que você está mudando é somente leitura.
adredx
171

Você precisa zombar do módulo e definir o espião sozinho:

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
fonte
4
Isso não parece certo. Eu entendo: babel-plugin-jest-hoist: The second argument of jest.mock must be a function.então o código nem está compilando.
Cam Jackson
3
Desculpe, eu atualizei meu código. Observe também que o caminho jest.mocké relativo ao arquivo de teste.
Andreas Köberle
1
Isso funcionou para mim, no entanto, não ao usar exportações padrão.
Iris Schaffer
4
@IrisSchaffer para que isso funcione com a exportação padrão que você precisa adicionar __esModule: trueao objeto simulado. Esse é o sinalizador interno usado pelo código transpilado para determinar se é um módulo es6 transpilado ou um módulo commonjs.
Johannes Lumpe
24
Zombando de exportações padrão: jest.mock('../dependency', () => ({ default: jest.fn() }))
Neob91 13/06
50

Para simular uma exportação padrão do módulo de dependência ES6 usando o 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);
  });
});

As outras opções não funcionaram no meu caso.

falsarella
fonte
6
qual é a melhor maneira de limpar isso, se eu só quero fazer um teste? dentro afterEach? `` `` afterEach (() => {jest.unmock (../ dependency ');}) `` `
nxmohamad
1
@falsarella o doMock realmente funciona nesse caso? Estou tendo problema muito semelhante e ele não faz nada quando eu estou tentando teste jest.doMock dentro específico, onde jest.mock para o módulo inteiro está funcionando corretamente
Progress1ve
1
@ Progress1ve você pode tentar usar jest.mock com mockImplementationOnce bem
Falsarella
1
Sim, essa é uma sugestão válida, no entanto, que exige que o teste seja o primeiro e eu não sou fã de escrever testes dessa maneira. Eu resolvi esses problemas importando o módulo externo e usando o spyOn em funções específicas.
Progress1ve
1
@ Progress1ve hmm eu quis colocar o mockImplementationOnce dentro de cada teste específico ... de qualquer maneira, estou feliz que você tenha encontrado uma solução :)
Falsarella
38

Adicionando mais à resposta de Andreas. Eu tive o mesmo problema com o código ES6, mas não queria alterar as importações. Isso parecia hacky. Então eu fiz isso

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

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

E adicionou dependency.js na pasta "__ zomba __" paralela ao dependency.js. Isso funcionou para mim. Além disso, isso me deu a opção de retornar dados adequados da implementação simulada. Certifique-se de fornecer o caminho correto para o módulo que você deseja zombar.

mdsAyubi
fonte
Obrigado por isso. Vai tentar. Gostei esta solução também - stackoverflow.com/a/38414160/1882064
arcseldon
O que eu gosto nessa abordagem é que ela oferece a possibilidade de fornecer uma simulação manual para todas as ocasiões em que você deseja simular um módulo específico. Eu, por exemplo, tenho um auxiliar de tradução, que é usado em muitos lugares. O __mocks__/translations.jsarquivo simplesmente padrão exportações jest.fn()em algo como:export default jest.fn((id) => id)
Iris Schaffer
Você também pode usar jest.genMockFromModulepara gerar simulações a partir de módulos. facebook.github.io/jest/docs/…
Varunkumar Nagarajan
2
Uma coisa a ser observada é que os módulos do ES6 zombados via export default jest.genMockFromModule('../dependency')terão todas as suas funções atribuídas dependency.defaultapós chamar `jest.mock ('.. dependency'), mas, caso contrário, se comportarão conforme o esperado.
Jhk
7
Como é a sua asserção de teste? Isso parece ser uma parte importante da resposta. expect(???)
stone
14

Avançando rapidamente para 2020, achei este link a solução. usando apenas a sintaxe do 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

Também uma coisa que você precisa saber (o que demorou um pouco para descobrir) é que você não pode chamar jest.mock () dentro do teste; você deve chamá-lo no nível superior do módulo. No entanto, você pode chamar mockImplementation () dentro de testes individuais se desejar configurar simulações diferentes para testes diferentes.

Andy
fonte
5

A pergunta já está respondida, mas você pode resolvê-la assim:

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
  });
});
Fino
fonte
Mas "require" é a sintaxe do CommonJS - o OP estava perguntando sobre os módulos ES6
Andy
@ Andy, obrigado pelo seu comentário, atualizei minha resposta. BTW mesma coisa na lógica.
Slim
2

Eu resolvi isso de outra maneira. Digamos que você tenha seu dependency.js

export const myFunction = () => { }

Eu crio um arquivo depdency.mock.js além dele com o seguinte conteúdo:

export const mockFunction = jest.fn();

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

e no teste, antes de importar o arquivo que possui a impedância que eu uso:

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

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

    functionThatCallsDep();

    expect(mockFunction).toHaveBeenCalled();

})
Felipe Leusin
fonte