Comment puis-je simuler une importation de module ES6 à l'aide de Jest?

281

Je commence à penser que ce n'est pas possible, mais je veux quand même demander.

Je veux tester qu'un de mes modules ES6 appelle un autre module ES6 d'une manière particulière. Avec Jasmine, c'est super facile -

Le code de l'application:

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

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

Et le code de test:

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

Quel est l'équivalent avec Jest? J'ai l'impression que c'est une chose si simple à vouloir faire, mais je me suis arraché les cheveux en essayant de le comprendre.

Le plus proche que je suis venu est de remplacer le imports par le requires et de les déplacer dans les tests / fonctions. Je ne veux pas faire quoi que ce soit.

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

Pour les points bonus, je serais ravi de faire fonctionner le tout lorsque la fonction à l'intérieur dependency.jsest une exportation par défaut. Cependant, je sais que l'espionnage des exportations par défaut ne fonctionne pas dans Jasmine (ou du moins je ne pourrais jamais le faire fonctionner), donc je n'espère pas que cela soit possible dans Jest.

Cam Jackson
la source
J'utilise Babel pour ce projet de toute façon, donc cela ne me dérange pas de continuer à transposer les imports en requires pour l'instant. Merci de m'avoir mis au courant.
Cam Jackson
que faire si j'ai la classe A et qu'elle appelle une fonction, disons doSomething () de la classe B, comment pouvons-nous nous moquer pour que la classe A fasse appel à la version moquée de la fonction de classe B doSomething ()
kailash yogeshwar
pour ceux qui veulent découvrir ce numéro plus github.com/facebook/jest/issues/936
omeralper

Réponses:

221

J'ai pu résoudre ce problème en utilisant un hack impliquant import *. Cela fonctionne même pour les exportations nommées et par défaut!

Pour une exportation nommée:

// 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 pour une exportation par défaut:

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

Comme Mihai Damian l'a souligné à juste titre ci-dessous, cela modifie l'objet du module dependency, et il va donc `` fuir '' vers d'autres tests. Donc, si vous utilisez cette approche, vous devez stocker la valeur d'origine, puis la rétablir à nouveau après chaque test. Pour ce faire facilement avec Jest, utilisez la méthode spyOn () au lieu de jest.fn()car elle prend en charge la restauration facile de sa valeur d'origine, évitant ainsi la «fuite» mentionnée précédemment.

Cam Jackson
la source
Merci d'avoir partagé. Je pense que le résultat net est similaire à cela - mais cela pourrait être plus propre - stackoverflow.com/a/38414160/1882064
arcseldon
65
Cela fonctionne, mais ce n'est probablement pas une bonne pratique. Les modifications apportées aux objets en dehors de la portée du test semblent persister entre les tests. Cela peut conduire ultérieurement à des résultats inattendus dans d'autres tests.
Mihai Damian
10
Au lieu d'utiliser jest.fn (), vous pouvez utiliser jest.spyOn () afin de pouvoir restaurer la méthode d'origine plus tard, afin qu'elle ne se transforme pas dans d'autres tests. J'ai trouvé un bel article sur différentes approches ici (jest.fn, jest.mock et jest.spyOn): medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c .
Martinsos
2
Juste une note: si le se dependencytrouve sur le même fichier que myModule, cela ne fonctionnera pas.
Lu Tran
3
Je pense que cela ne fonctionnera pas avec Typescript, l'objet que vous mutez est en lecture seule.
adredx
172

Vous devez vous moquer du module et définir l'espion par vous-même:

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
la source
4
Cela ne semble pas correct. Je reçois: babel-plugin-jest-hoist: The second argument of jest.mock must be a function.Donc, le code ne compile même pas.
Cam Jackson
3
Désolé, j'ai mis à jour mon code. Veuillez également noter que le chemin d'accès jest.mockest relatif au fichier de test.
Andreas Köberle
1
Cela n'a pas fonctionné pour moi, cependant, pas lors de l'utilisation des exportations par défaut.
Iris Schaffer
4
@IrisSchaffer pour que cela fonctionne avec l'exportation par défaut que vous devez ajouter __esModule: trueà l'objet maquette. C'est l'indicateur interne utilisé par le code transpilé pour déterminer s'il s'agit d'un module es6 transpilé ou d'un module commonjs.
Johannes Lumpe
24
Exportations par défaut jest.mock('../dependency', () => ({ default: jest.fn() }))
moqueuses
50

Pour simuler une exportation par défaut du module de dépendance ES6 à l'aide de 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);
  });
});

Les autres options n'ont pas fonctionné pour mon cas.

falsarella
la source
6
quelle est la meilleure façon de nettoyer ça si je veux juste faire un test? à l'intérieur après chaque? `` `` afterEach (() => {jest.unmock (../ dependency ');}) `` ``
nxmohamad
1
@falsarella doMock fonctionne-t-il réellement dans ce cas? J'ai un problème très similaire et cela ne fait rien lorsque j'essaie de jest.doMock dans un test spécifique, où jest.mock pour l'ensemble du module fonctionne correctement
Progress1ve
1
@ Progress1ve, vous pouvez également utiliser jest.mock avec mockImplementationOnce
falsarella
1
Oui, c'est une suggestion valable, mais cela nécessite que le test soit le premier et je ne suis pas fan d'écrire des tests de cette manière. J'ai contourné ces problèmes en important un module externe et en utilisant spyOn sur des fonctions spécifiques.
Progress1ve
1
@ Progress1ve hmm Je voulais placer le mockImplementationOnce à l'intérieur de chaque test spécifique ... de toute façon, je suis heureux que vous ayez trouvé une solution :)
falsarella
38

Ajout de plus à la réponse d'Andreas. J'ai eu le même problème avec le code ES6 mais je ne voulais pas muter les importations. Cela semblait bizarre. Alors j'ai fait ça

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

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

Et ajouté dependency.js dans le dossier "__ mocks __" parallèle à dependency.js. Cela a fonctionné pour moi. De plus, cela m'a donné la possibilité de renvoyer des données appropriées à partir d'une implémentation fictive. Assurez-vous de donner le chemin correct au module que vous souhaitez simuler.

mdsAyubi
la source
Merci pour cela. Je vais essayer. A également aimé cette solution - stackoverflow.com/a/38414160/1882064
arcseldon
Ce que j'aime dans cette approche, c'est qu'elle vous donne la possibilité de fournir une maquette manuelle pour toutes les occasions où vous souhaitez simuler un module spécifique. Par exemple, j'ai un assistant de traduction, qui est utilisé dans de nombreux endroits. Le __mocks__/translations.jsfichier exporte simplement par défaut jest.fn()dans quelque chose comme:export default jest.fn((id) => id)
Iris Schaffer
Vous pouvez également utiliser jest.genMockFromModulepour générer des simulations à partir de modules. facebook.github.io/jest/docs/…
Varunkumar Nagarajan
2
Une chose à noter est que les modules ES6 simulés via export default jest.genMockFromModule('../dependency')auront toutes leurs fonctions assignées à dependency.defaultaprès avoir appelé `jest.mock ('.. dépendance'), mais se comportent autrement comme prévu.
jhk
7
À quoi ressemble votre assertion de test? Cela semble être une partie importante de la réponse. expect(???)
stone
14

Avance rapide jusqu'en 2020, j'ai trouvé ce lien comme la solution. en utilisant uniquement la syntaxe du module 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

Une chose que vous devez également savoir (ce qui m'a pris du temps à comprendre) est que vous ne pouvez pas appeler jest.mock () dans le test; vous devez l'appeler au niveau supérieur du module. Cependant, vous pouvez appeler mockImplementation () dans des tests individuels si vous souhaitez configurer différents mocks pour différents tests.

Andy
la source
5

La question est déjà répondue mais vous pouvez la résoudre comme ceci:

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
  });
});
Svelte
la source
Mais "exiger" est la syntaxe CommonJS - OP posait des questions sur les modules ES6
Andy
@Andy merci pour votre commentaire, j'ai mis à jour ma réponse. BTW même chose dans la logique.
Slim
2

J'ai résolu cela d'une autre manière. Disons que vous avez votre dependency.js

export const myFunction = () => { }

Je crée en plus un fichier depdency.mock.js avec le contenu suivant:

export const mockFunction = jest.fn();

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

et dans le test, avant d'importer le fichier qui a la dépendance que j'utilise:

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

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

    functionThatCallsDep();

    expect(mockFunction).toHaveBeenCalled();

})
Felipe Leusin
la source