Как я могу издеваться над импортом модуля ES6 с помощью Jest?

281

Я начинаю думать, что это невозможно, но я все равно хочу спросить.

Я хочу проверить, что один из моих модулей ES6 вызывает другой модуль ES6 определенным образом. С Жасмином это очень просто -

Код приложения:

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

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

И тестовый код:

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

Что эквивалентно с Джестом? Я чувствую, что это такая простая вещь, которую я хочу сделать, но я рвал на себе волосы, пытаясь понять это.

Самое близкое, что я получил, - это заменить imports на requires и поместить их в тесты / функции. Ничего из того, что я хочу делать.

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

Что касается бонусных баллов, я бы хотел, чтобы все это работало, когда функция внутри dependency.js- экспорт по умолчанию. Тем не менее, я знаю, что слежка за экспортом по умолчанию не работает в Jasmine (или, по крайней мере, я никогда не смогу заставить его работать), поэтому я не надеюсь, что это возможно в Jest.

Кэм Джексон
источник
В любом случае, я использую Babel для этого проекта, так что пока я не против продолжать переходить importк requires. Спасибо за внимание, хотя.
Кэм Джексон
Что если у меня есть класс ts A, и он вызывает некоторую функцию, скажем, doSomething () класса B, как мы можем насмехаться так, чтобы класс A делал вызов моделируемой версии функции класса B doSomething ()
kailash yogeshwar
для тех, кто хочет узнать об этой проблеме подробнее github.com/facebook/jest/issues/936
omeralper

Ответы:

221

Я смог решить эту проблему с помощью взлома с участием import *. Это даже работает как для именованных, так и для экспорта по умолчанию!

Для именованного экспорта:

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

Или для экспорта по умолчанию:

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

Как справедливо заметил Михай Дамиан ниже, это мутирует объект модуля dependency, и поэтому он «просочится» в другие тесты. Поэтому, если вы используете этот подход, вы должны сохранить исходное значение и затем устанавливать его снова после каждого теста. Чтобы сделать это легко с помощью Jest, используйте метод spyOn () вместо того, jest.fn()потому что он поддерживает простое восстановление исходного значения, поэтому избегая ранее упомянутого «утечки».

Кэм Джексон
источник
Спасибо, что поделился. Я думаю, что чистый результат похож на это - но это может быть чище - stackoverflow.com/a/38414160/1882064
arcseldon
65
Это работает, но это, вероятно, не очень хорошая практика. Кажется, что изменения между объектами, выходящими за рамки теста, сохраняются между тестами. Это может впоследствии привести к неожиданным результатам в других тестах.
Михай Дамиан
10
Вместо использования jest.fn () вы можете использовать jest.spyOn (), чтобы впоследствии можно было восстановить исходный метод, чтобы он не попадал в другие тесты. Я нашел хорошую статью о различных подходах здесь (jest.fn, jest.mock и jest.spyOn): medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c .
Мартинсос
2
Просто примечание: если dependencyфайл находится в одном файле myModule, он не будет работать.
Лу Тран
3
Я думаю, что это не будет работать с Typescript, объект, который вы изменяете, только для чтения.
adredx
172

Вы должны смоделировать модуль и установить шпиона самостоятельно:

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);
  });
});
Андреас Кёберле
источник
4
Это не кажется правильным. Я понимаю: babel-plugin-jest-hoist: The second argument of jest.mock must be a function.так что код даже не компилируется.
Кэм Джексон
3
Извините, я обновил свой код. Также обратите внимание, что путь в jest.mockотносительно тестового файла.
Андреас Кёберле
1
Это сработало для меня, однако, не при использовании экспорта по умолчанию.
Ирис Шаффер
4
@IrisSchaffer Для того, чтобы эта работа работала с экспортом по умолчанию, вы должны добавить его __esModule: trueк фиктивному объекту. Это внутренний флаг, используемый переносимым кодом для определения, является ли это переносимым модулем es6 или модулем commonjs.
Йоханнес Лумпе
24
Насмешливый экспорт по умолчанию: jest.mock('../dependency', () => ({ default: jest.fn() }))
Neob91
50

Чтобы смоделировать экспорт модуля зависимостей ES6 по умолчанию с помощью 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);
  });
});

Другие варианты не работали для моего случая.

falsarella
источник
6
Какой лучший способ убрать это, если я просто хочу сделать для одного теста? внутри после каждого? `` `` afterEach (() => {jest.unmock (../ зависимость));}) `` ``
nxmohamad
1
@falsarella действительно ли в этом случае работает doMock? У меня очень похожая проблема, и она ничего не делает, когда я пытаюсь jest.doMock внутри конкретного теста, где jest.mock для всего модуля работает правильно
Progress1ve
1
@ Progress1ve, вы также можете попробовать использовать jest.mock с mockImplementationOnce
falsarella
1
Да, это правильное предложение, однако оно требует, чтобы тест был первым, и я не фанат написания тестов таким образом. Я справился с этими проблемами, импортировав внешний модуль и используя spyOn для определенных функций.
Progress1ve
1
@ Progress1ve хмм Я хотел поместить mockImplementationOnce внутри каждого конкретного теста ... в любом случае, я рад, что вы нашли решение :)
falsarella
38

Добавляю ещё Андреасу ответ. У меня была та же проблема с кодом ES6, но я не хотел изменять импорт. Это выглядело хакерским. Так я и сделал

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

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

И добавил зависимости.js в папку «__ mocks __» параллельно с зависимостью.js. Это сработало для меня. Кроме того, это дало мне возможность вернуть подходящие данные из фиктивной реализации. Убедитесь, что вы указали правильный путь к модулю, который хотите смоделировать.

mdsAyubi
источник
Спасибо за это. Попробую. Понравилось и это решение - stackoverflow.com/a/38414160/1882064
arcseldon
Что мне нравится в этом подходе, так это то, что он дает вам возможность предоставить один ручной макет для всех случаев, когда вы хотите смоделировать конкретный модуль. У меня, например, есть помощник по переводу, который используется во многих местах. __mocks__/translations.jsФайл просто по умолчанию экспорт jest.fn()в чем - то вроде:export default jest.fn((id) => id)
Iris Шаффер
Вы также можете использовать jest.genMockFromModuleдля генерации макетов из модулей. facebook.github.io/jest/docs/…
Варункумар Нагараджан,
2
Стоит отметить, что для модулей ES6, которые подвергаются мошенничеству, export default jest.genMockFromModule('../dependency')будут назначены все их функции dependency.defaultпосле вызова `jest.mock ('.. dependency'), но в остальном они будут работать так, как ожидается.
JHK
7
Как выглядит ваше тестовое утверждение? Это кажется важной частью ответа. expect(???)
камень
14

Перейдя к 2020 году, я нашел эту ссылку решением проблемы. используя только синтаксис модуля 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

Также одна вещь, которую вам нужно знать (на что у меня ушло некоторое время, чтобы понять), что вы не можете вызвать jest.mock () внутри теста; Вы должны вызвать его на верхнем уровне модуля. Однако вы можете вызывать mockImplementation () внутри отдельных тестов, если вы хотите настроить разные макеты для разных тестов.

Энди
источник
5

На вопрос уже дан ответ, но вы можете решить его следующим образом:

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
  });
});
Тонкий
источник
Но "require" - это синтаксис CommonJS - OP спрашивал о модулях ES6
Andy
@ Энди, спасибо за твой комментарий, я обновил свой ответ. Кстати, то же самое в логике.
Слим
2

Я решил это по-другому. Допустим, у вас есть ваш dependency.js

export const myFunction = () => { }

Помимо этого, я создаю файл depdency.mock.js со следующим содержимым:

export const mockFunction = jest.fn();

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

и в тесте, прежде чем импортировать файл с зависимостью, которую я использую:

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

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

    functionThatCallsDep();

    expect(mockFunction).toHaveBeenCalled();

})
Фелипе Леусин
источник