Як знущатися над імпортованою іменованою функцією в Jest, коли модуль розблоковано


111

У мене є такий модуль, який я намагаюся протестувати в Jest:

// myModule.js

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

export function testFn() {
  otherFn();

  // do other things
}

Як показано вище, він експортує деякі названі функції та, що важливо, testFnвикористовує otherFn.

Коли я пишу свій модульний тест для Jest testFn, я хочу знущатися над цією otherFnфункцією, оскільки не хочу, щоб помилки otherFnвпливали на мій модульний тест testFn. Моя проблема полягає в тому, що я не впевнений, що найкращий спосіб це зробити:

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

Будь-яка допомога / розуміння вітається.


7
Я б не робив цього. Знущання, як правило, не те, що ви хочете зробити. І якщо вам потрібно щось знущатись (через здійснення дзвінків на сервер / тощо), то вам слід просто витягти otherFnв окремий модуль і знущатися над цим.
kentcdodds

2
Я також тестую з тим самим підходом, який використовує @jrubins. Перевірити поведінку тих, function Aхто телефонує, function Bале я не хочу виконувати реальну реалізацію, function Bтому що я хочу просто перевірити логіку, реалізовану вfunction A
jplaza

44
@kentcdodds, не могли б ви пояснити, що ви маєте на увазі під словами "глузування - це, як правило, не те, що ви хочете зробити"? Це здається досить широким (надто широким?) Твердженням, оскільки насмішка, безумовно, є тим, що часто використовується, імовірно, з (принаймні деяких) вагомих причин. Отже, ви, мабуть, маєте на увазі, чому знущання тут може бути недобрим , або ви взагалі маєте на увазі?
Ендрю Віллемс,

2
Часто глузуванням є тестування деталей реалізації. Особливо на цьому рівні це призводить до тестів, які насправді перевіряють не набагато більше, ніж той факт, що ваші тести працюють (не те, що працює ваш код).
kentcdodds

3
Для запису, з моменту написання цього запитання багато років тому, я відтоді змінив мою мелодію щодо того, скільки глузувань я хотів би зробити (і більше не робити такого глузування). У наші дні я дуже погоджуюся з @kentcdodds та його філософією тестування (і настійно рекомендую його блог та @testing-library/reactбудь-які інші реактори), але я знаю, що це спірна тема.
Джон

Відповіді:


100

Використовуйте jest.requireActual()всерединуjest.mock()

jest.requireActual(moduleName)

Повертає фактичний модуль замість фіктивного, обходячи всі перевірки щодо того, чи повинен модуль отримувати макетну реалізацію чи ні.

Приклад

Я віддаю перевагу такому стислому використанню там, де вам потрібно і поширити його у межах повернутого об’єкта:

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

На цей метод також посилається документація Jest's Manual Mocks (біля кінця Прикладів ):

Щоб гарантувати, що макет вручну та його реальна реалізація залишаються синхронізованими, може бути корисним вимагати використання реального модуля jest.requireActual(moduleName)у вашому макеті вручну та внесення змін до нього з допомогою функцій макету перед експортом.


4
Fwiw, ви можете зробити це ще більш стислим, видаливши returnтвердження та обернувши тіло функції стрілки в дужки: наприклад. jest.mock('./myModule', () => ({ ...jest.requireActual('./myModule'), otherFn: () => {}}))
Nick F

2
Це чудово працювало! Це має бути прийнятою відповіддю.
JRJurman

2
...jest.requireActualне працював у мене належним чином, тому що у мене є накладення ...require.requireActual
Tzahi Leh

1
Як би ви протестували те, що otherFunбуло викликано в цьому випадку? ПрипускаючиotherFn: jest.fn()
Стевула

1
@Stevula Я оновив свою відповідь, щоб показати реальний приклад використання. Я показую mockReturnValueметод, щоб краще продемонструвати, що висміювана версія отримує виклик замість оригіналу, але якщо ви дійсно просто хочете побачити, чи був він викликаний без твердження щодо поверненого значення, ви можете скористатись матч-пошуком.toHaveBeenCalled() .
гфулам

36
import m from '../myModule';

Не працює для мене, я використовував:

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

m.otherFn = jest.fn();

5
Як би ви відновили початкову функціональність otherFn після тесту, щоб вона не заважала іншим testS?
Aequitas

1
Я думаю, ви можете налаштувати жарт для очищення макетів після кожного тесту? З документації: "Доступна опція конфігурації clearMocks для автоматичного очищення макетів між тестами.". Ви можете встановити clearkMocks: trueконфігурацію jest package.json. facebook.github.io/jest/docs/en/mock-function-api.html
Коул

2
якщо це така проблема зміни глобального стану, ви завжди можете зберегти оригінальну функціональність всередині якоїсь тестової змінної та повернути її після тесту
bobu

1
const оригінал; beforeAll (() => {original = m.otherFn; m.otherFn = jest.fn ();}) afterAll (() => {m.otherFn = original;}) це повинно працювати, проте я не тестував it
bobu

Дуже дякую! Це вирішило мою проблему.
Слободан Красавчевич

25

Схоже, я запізнився на цю вечірку, але так, це можливо.

testFn просто потрібно зателефонувати otherFn за допомогою модуля .

Якщо testFnдля виклику використовується модуль, otherFnтоді модуль експортується дляotherFn може бути знущанням і testFnбуде викликати макет.


Ось робочий приклад:

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

2
Це, по суті, версія ES6 підходу, який використовується у Facebook і описаний розробником Facebook в середині цього допису .
Брайан Адамс,

замість того, щоб імпортувати myModule в себе, просто зателефонуйтеexports.otherFn()
andrhamm

3
@andrhamm exportsне існує в ES6. Виклик exports.otherFn()працює зараз, оскільки ES6 компілюється до попереднього синтаксису модуля, але він порушиться, коли ES6 підтримується рідно.
Брайан Адамс,

Маючи саме цю проблему прямо зараз, і я впевнений, що стикався з цією проблемою раніше. Мені довелося видалити вантаж експорту. <methodname>, щоб допомогти дереву трястись, і багато тестів було порушено. Я подивлюсь, чи це матиме якийсь ефект, але це здається настільки шаленим. Я стикався з цією проблемою неодноразово, і, як вже було сказано в інших відповідях, такі речі, як babel-plugin-rewire або навіть краще, npmjs.com/package/rewiremock, які я впевнений, що можу зробити і вище.
Astridax,

Чи можна замість глузувати повернене значення, глузувати кидок? Редагувати: можна, ось як stackoverflow.com/a/50656680/2548010
Великі гроші

10

Переписаний код не дозволить babel отримати прив'язку, на яку otherFn()посилається. Якщо ви використовуєте функціонування, ви зможете досягти глузування 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();

Але, як @kentcdodds згадувалось у попередньому коментарі, ви, мабуть, не хотіли б глузувати otherFn(). Швидше, просто напишіть нову специфікацію otherFn()та знущайтеся над усіма необхідними дзвінками, які вона робить.

Так, наприклад, якщо otherFn()робить запит http ...

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

Тут ви хочете знущатися http.getта оновлювати свої твердження на основі ваших знущаних реалізацій.

// myModule.test.js
jest.mock('http', () => ({
  get: jest.fn(() => {
    console.log('test');
  }),
}));

1
що, якщо otherFn і testFn використовуються кількома іншими модулями? вам потрібно буде встановити http макет у всіх тестових файлах, які використовують (хоч би глибокий стек) ці 2 модулі? Крім того, якщо у вас вже є тест на testFn, чому б не заглушити testFn безпосередньо замість http в модулях, що використовує testFn?
розірвано

1
тож, якщо otherFnпорушено, не вдасться виконати всі тести, які залежать від цього. Крім того otherFn, якщо всередині є 5 ifs, можливо, вам доведеться перевірити, testFnчи добре ви працюєте з усіма цими випадками. У вас буде набагато більше шляхів коду для тестування зараз.
Totty.js

6

Я знаю, що це давно задавали, але я просто зіткнувся з цією ситуацією і нарешті знайшов рішення, яке могло б спрацювати. Тож я подумав поділитися тут.

Для модуля:

// myModule.js

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

export function testFn() {
  otherFn();

  // do other things
}

Ви можете перейти до наступного:

// myModule.js

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

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

  // do other things
}

експортуючи їх як константи замість функцій. Я вважаю, що проблема пов’язана з підняттям у JavaScript та використанням constзапобігає такій поведінці.

Тоді у вашому тесті ви можете мати щось на зразок наступного:

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

Тепер ваші знущання повинні працювати так, як ви зазвичай очікуєте.


2

Я вирішив свою проблему поєднанням відповідей, які я знайшов тут:

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

0

Зверху на першу відповідь тут ви можете використовувати babel-plugin-rewire для знущання над імпортованою іменованою функцією. Ви можете поверхово ознайомитись із розділом щодо перейменування іменованої функції .

Однією з безпосередніх переваг для вашої ситуації тут є те, що вам не потрібно змінювати спосіб виклику іншої функції з вашої функції.


Як налаштувати babel-plugin-rewire для роботи з node.js?
Тимур Гілаурі,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.