Como simular uma função nomeada importada no Jest quando o módulo é desbloqueado

111

Tenho o seguinte módulo que estou tentando testar no Jest:

// myModule.js

export function otherFn() {
  console.log('do something');
}

export function testFn() {
  otherFn();

  // do other things
}

Conforme mostrado acima, ele exporta algumas funções nomeadas e testFnusos importantes otherFn.

De brincadeira, quando estou escrevendo meu teste de unidade para testFn, quero simular a otherFnfunção porque não quero que erros no otherFnafetem meu teste de unidade para testFn. Meu problema é que não tenho certeza da melhor maneira de fazer isso:

// myModule.test.js
jest.unmock('myModule');

import { testFn, otherFn } from 'myModule';

describe('test category', () => {
  it('tests something about testFn', () => {
    // I want to mock "otherFn" here but can't reassign
    // a.k.a. can't do otherFn = jest.fn()
  });
});

Qualquer ajuda / visão é apreciada.

Jon Rubins
fonte
7
Eu não faria isso. Zombar geralmente não é algo que você queira fazer de qualquer maneira. E se você precisar simular algo (devido a fazer chamadas ao servidor / etc.), Você deve apenas extrair otherFnem um módulo separado e simular isso.
kentcdodds
2
Também estou testando com a mesma abordagem que o @jrubins usa. Teste o comportamento de function Aquem chama, function Bmas não quero executar a implementação real de function Bporque quero apenas testar a lógica implementada emfunction A
jplaza
44
@kentcdodds, Você poderia esclarecer o que você quer dizer com "Zombar geralmente não é algo que você queira fazer."? Essa parece ser uma afirmação bastante ampla (excessivamente ampla?), Já que zombar é certamente algo que é freqüentemente usado, presumivelmente por (pelo menos algumas) boas razões. Então, talvez você esteja se referindo a por que zombar pode não ser bom aqui , ou você realmente quer dizer em geral?
Andrew Willems
2
Freqüentemente, simular é testar detalhes de implementação. Especialmente nesse nível, isso leva a testes que não estão realmente validando muito mais do que o fato de que seus testes funcionam (não que seu código funcione).
kentcdodds
3
Para o registro, desde que escrevi esta pergunta anos atrás, eu mudei meu tom sobre o quanto eu gostaria de zombar (e não zombar mais desse jeito). Atualmente, eu concordo plenamente com @kentcdodds e sua filosofia de teste (e recomendo seu blog e @testing-library/reactpara qualquer Reacters por aí), mas sei que este é um assunto controverso.
Jon Rubins

Respostas:

100

Use jest.requireActual()dentrojest.mock()

jest.requireActual(moduleName)

Retorna o módulo real em vez de uma simulação, ignorando todas as verificações sobre se o módulo deve receber uma implementação simulada ou não.

Exemplo

Prefiro esse uso conciso, onde você exige e espalha dentro do objeto retornado:

// myModule.test.js

jest.mock('./myModule.js', () => (
  {
    ...(jest.requireActual('./myModule.js')),
    otherFn: jest.fn()
  }
))

import { otherFn } from './myModule.js'

describe('test category', () => {
  it('tests something about otherFn', () => {
    otherFn.mockReturnValue('foo')
    expect(otherFn()).toBe('foo')
  })
})

Este método também é referenciado na documentação do Manual Mocks de Jest (próximo ao final dos Exemplos ):

Para garantir que um mock manual e sua implementação real fiquem em sincronia, pode ser útil exigir que o módulo real seja usado jest.requireActual(moduleName)em seu mock manual e corrigí-lo com funções mock antes de exportá-lo.

gfullam
fonte
4
Fwiw, você pode tornar isso ainda mais conciso removendo a returninstrução e envolvendo o corpo da função de seta entre parênteses: por exemplo. jest.mock('./myModule', () => ({ ...jest.requireActual('./myModule'), otherFn: () => {}}))
Nick F
2
Isso funcionou muito bem! Esta deve ser a resposta aceita.
JRJurman
2
...jest.requireActualnão funcionou corretamente para mim porque eu tenho o aliasing de caminho usando o babel. Funciona com ...require.requireActualou depois de remover o aliasing do caminho
Tzahi Leh
1
Como você testaria que otherFunfoi chamado neste caso? AssumindootherFn: jest.fn()
Stevula
1
@Stevula Eu atualizei minha resposta para mostrar um exemplo de uso real. Eu mostro o mockReturnValuemétodo para demonstrar melhor que a versão simulada está sendo chamada em vez da original, mas se você realmente quiser apenas ver se ela foi chamada sem declarar contra o valor de retorno, você pode usar o matcher jest .toHaveBeenCalled().
gfullam
36
import m from '../myModule';

Não funciona para mim, eu usei:

import * as m from '../myModule';

m.otherFn = jest.fn();
bobu
fonte
5
Como você restauraria a funcionalidade original do otherFn após o teste para que não interfira com outros testes?
Aequitas de
1
Eu acho que você pode configurar o jest para limpar as simulações após cada teste? Dos documentos: "A opção de configuração clearMocks está disponível para limpar simulações automaticamente entre os testes.". Você pode definir a configuração de clearkMocks: truejest package.json. facebook.github.io/jest/docs/en/mock-function-api.html
Cole
2
se for esse o problema de alterar o estado global, você sempre pode armazenar a funcionalidade original dentro de algum tipo de variável de teste e trazê-la de volta após o teste
bobu
1
const original; beforeAll (() => {original = m.otherFn; m.otherFn = jest.fn ();}) afterAll (() => {m.otherFn = original;}) deve funcionar, porém eu não testei it
bobu
Muito obrigado! Isso resolveu meu problema.
Slobodan Krasavčević
25

Parece que estou atrasado para esta festa, mas sim, isso é possível.

testFnsó precisa chamar otherFn usando o módulo .

Se testFnusar o módulo para chamar otherFn, a exportação do módulo para otherFnpode ser testFnsimulada e chamará a simulação.


Aqui está um exemplo prático:

myModule.js

import * as myModule from './myModule';  // import myModule into itself

export function otherFn() {
  return 'original value';
}

export function testFn() {
  const result = myModule.otherFn();  // call otherFn using the module

  // do other things

  return result;
}

myModule.test.js

import * as myModule from './myModule';

describe('test category', () => {
  it('tests something about testFn', () => {
    const mock = jest.spyOn(myModule, 'otherFn');  // spy on otherFn
    mock.mockReturnValue('mocked value');  // mock the return value

    expect(myModule.testFn()).toBe('mocked value');  // SUCCESS

    mock.mockRestore();  // restore otherFn
  });
});
Brian Adams
fonte
2
Esta é essencialmente uma versão ES6 da abordagem usada no Facebook e descrita por um desenvolvedor do Facebook no meio deste post .
Brian Adams
em vez de importar myModule para si mesmo, basta ligar paraexports.otherFn()
andrhamm
3
@andrhamm exportsnão existe no ES6. A chamada exports.otherFn()funciona agora porque o ES6 está sendo compilado com uma sintaxe de módulo anterior, mas será interrompido quando o ES6 for suportado nativamente.
Brian Adams
Estou tendo exatamente esse problema agora e tenho certeza de que já o encontrei antes. Tive que remover um monte de exportações. <methodname> para ajudar a árvore a balançar e muitos testes quebraram. Vou ver se isso tem algum efeito, mas parece tão hacky. Já me deparei com esse problema várias vezes e, como outras respostas já disseram, coisas como babel-plugin-rewire ou ainda melhor, npmjs.com/package/rewiremock , que tenho quase certeza de que pode fazer o mesmo.
Astridax
É possível simular um valor de retorno, simular um lançamento? Editar: você pode, aqui está como stackoverflow.com/a/50656680/2548010
Big Money
10

O código transpilado não permitirá que o babel recupere o vínculo ao qual otherFn()está se referindo. Se você usar uma expessão de função, deverá conseguir realizar a simulação otherFn().

// myModule.js
exports.otherFn = () => {
  console.log('do something');
}

exports.testFn = () => {
  exports.otherFn();

  // do other things
}

 

// myModule.test.js
import m from '../myModule';

m.otherFn = jest.fn();

Mas, como @kentcdodds mencionou no comentário anterior, você provavelmente não gostaria de zombar otherFn(). Em vez disso, basta escrever uma nova especificação otherFn()e simular todas as chamadas necessárias que ele está fazendo.

Por exemplo, se otherFn()estiver fazendo uma solicitação http ...

// myModule.js
exports.otherFn = () => {
  http.get('http://some-api.com', (res) => {
    // handle stuff
  });
};

Aqui, você deseja simular http.gete atualizar suas asserções com base em suas implementações simuladas .

// myModule.test.js
jest.mock('http', () => ({
  get: jest.fn(() => {
    console.log('test');
  }),
}));
Vutran
fonte
1
e se otherFn e testFn forem usados ​​por vários outros módulos? você precisaria configurar a simulação http em todos os arquivos de teste que usam (por mais profundo que seja a pilha) esses 2 módulos? Além disso, se você já tem um teste para testFn, por que não stub testFn diretamente em vez de http nos módulos que usam testFn?
rickmed em
1
então se otherFnestiver quebrado, irá falhar em todos os testes que dependem daquele. Além disso, se otherFntiver 5 ifs dentro, você pode precisar testar se testFnestá funcionando bem para todos os subcasos. Você terá muitos mais caminhos de código para testar agora.
Totty.js
6

Sei que isso foi perguntado há muito tempo, mas me deparei com essa mesma situação e finalmente encontrei uma solução que funcionaria. Então eu pensei em compartilhar aqui.

Para o módulo:

// myModule.js

export function otherFn() {
  console.log('do something');
}

export function testFn() {
  otherFn();

  // do other things
}

Você pode alterar o seguinte:

// myModule.js

export const otherFn = () => {
  console.log('do something');
}

export const testFn = () => {
  otherFn();

  // do other things
}

exportá-los como constantes em vez de funções. Eu acredito que o problema tem a ver com o içamento em JavaScript e o uso de constprevenir esse comportamento.

Então, em seu teste, você pode ter algo como o seguinte:

import * as myModule from 'myModule';


describe('...', () => {
  jest.spyOn(myModule, 'otherFn').mockReturnValue('what ever you want to return');

  // or

  myModule.otherFn = jest.fn(() => {
    // your mock implementation
  });
});

Seus mocks agora devem funcionar como você normalmente esperaria.

Jack Kinsey
fonte
2

Resolvi meu problema com uma combinação das respostas que encontrei aqui:

myModule.js

import * as myModule from './myModule';  // import myModule into itself

export function otherFn() {
  return 'original value';
}

export function testFn() {
  const result = myModule.otherFn();  // call otherFn using the module

  // do other things

  return result;
}

myModule.test.js

import * as myModule from './myModule';

describe('test category', () => {
  let otherFnOrig;

  beforeAll(() => {
    otherFnOrig = myModule.otherFn;
    myModule.otherFn = jest.fn();
  });

  afterAll(() => {
    myModule.otherFn = otherFnOrig;
  });

  it('tests something about testFn', () => {
    // using mock to make the tests
  });
});
demaroar
fonte
0

Além da primeira resposta aqui, você pode usar o babel-plugin-rewire para simular a função nomeada importada também. Você pode verificar a seção superficialmente para religamento de funções nomeadas .

Um dos benefícios imediatos para sua situação aqui é que você não precisa alterar como você chama a outra função de sua função.

código de Schrodinger
fonte
Como configurar o babel-plugin-rewire para funcionar com node.js?
Timur Gilauri