jestjs / jest

Delightful JavaScript Testing.
https://jestjs.io
MIT License
44.21k stars 6.46k forks source link

How to mock specific module function? #936

Closed seibelj closed 8 years ago

seibelj commented 8 years ago

I'm struggling with something that I think should be both easy and obvious, but for whatever reason I can't figure it out.

I have a module. It exports multiple functions. Here is myModule.js:

export function foo() {...}
export function bar() {...}
export function baz() {...}

I unmock the module for testing.

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

However, I need to mock foo, because it makes ajax calls to my backend. I want every function in this file to remain unmocked, expect for foo, which I want to be mocked. And the functions bar and baz make calls internally to foo, so when my test calls bar(), the unmocked bar will call the mocked foo.

It appears in the jest documentation that calls to unmock and mock operate on the entire module. How can I mock a specific function? Arbitrarily breaking up my code into separate modules so they can be tested properly is ridiculous.

seibelj commented 8 years ago

Upon deeper analysis, it appears that jest-mock generates an AST for the whole module, then uses that AST to create a mocked module that conforms to original's exports: https://github.com/facebook/jest/tree/master/packages/jest-mock

Other testing frameworks, such as Python's mock (https://docs.python.org/3/library/unittest.mock-examples.html), let you mock specific functions. This is a fundamental testing concept.

I highly recommend the ability to mock a portion of a module. I think that jest-mock should be changed to conditionally ignore exports from mocking, and reference the original implementation.

cpojer commented 8 years ago

You can do:

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

const myModule = require('myModule');
myModule.foo = jest.fn();

See http://facebook.github.io/jest/docs/api.html#mock-functions

seibelj commented 8 years ago

I think you have a fundamental misunderstanding of how require works. When you call require(), you don't get an instance of the module. You get an object with references to the module's functions. If you overwrite a value in the required module, your own reference is overwritten, but the implementation keeps the original references.

In your example, if you call myModule.foo(), yes, you will call the mocked version. But if you call myModule.bar(), which internally calls foo(), the foo it references is not your overwritten version. If you don't believe me, you can test it out.

Therefore, the example you described is inadequate for the problem I have. Do you know something I don't?

seibelj commented 8 years ago

@cpojer

cpojer commented 8 years ago

I do believe I understand this quite well. The way babel compiles modules however doesn't make this easier to understand and I understand your confusion. I don't know exactly how this would behave in a real ES2015 environment with modules, mainly because no such environment exists right now (except maybe latest versions of Chrome Canary, which I haven't tried yet). In order to explain what happens, we have to look at the compiled output of your babel code. It will look something like this:

var foo = function foo() {};
var bar = function bar() { foo(); };

exports.foo = foo;
exports.bar = bar;

In this case, it is indeed correct that you cannot mock foo and I apologize for not reading your initial issue correctly, however it did not make any assumption on how foo was called, so I assumed it was exports.foo(). Supporting the above by mocking a function after requiring a module is impossible in JavaScript – there is (almost) no way to retrieve the binding that foo refers to and modify it.

However, if you change your code to this:

var foo = function foo() {};
var bar = function bar() { exports.foo(); };

exports.foo = foo;
exports.bar = bar;

and then in your test file you do:

var module = require('../module');
module.foo = jest.fn();
module.bar();

it will work just as expected. This is what we do at Facebook where we don't use ES2015.

While ES2015 modules may have immutable bindings for what they export, the underlying compiled code that babel compiles to right now doesn't enforce any such constraints. I see no way currently to support exactly what you are asking in a strict ES2015 module environment with natively supported modules. The way that jest-mock works is that it runs the module code in isolation and then retrieves the metadata of a module and creates mock functions. Again, in this case it won't have any way to modify the local binding of foo. If you do have ideas on how to effectively implement this, please contribute here or with a pull request. I'd like to remind you that we have a code of conduct for this project that you can read up on here: https://code.facebook.com/pages/876921332402685/open-source-code-of-conduct

The right solution in your example is not to mock foo but to mock the higher-level API that foo is calling (such as XMLHttpRequest or the abstraction that you use instead).

seibelj commented 8 years ago

@cpojer Thank you for your detailed explanation. I'm sorry if I offended you with my language, I am very efficient with my engineering writing and I want to get my point across ASAP. To put things into perspective, I spent 5 hours of time trying to understand this issue and wrote 2 detailed comments, then you closed it with a brief message that completely missed the point of both of my statements. That is why my next message said you had a "fundamental misunderstanding", because either 1) you did not understand the point I was making, or 2) you didn't understand require(), which thankfully was option 1.

I will ponder a possible solution to my problem, to get around it for now I mocked a lower level API, but there should definitely be a way to mock the function directly, as that would be quite useful.

cpojer commented 8 years ago

I agree it would be useful to be able to do this, but there isn't a good way in JS to do it without (probably slow) static analysis upfront :(

lnhrdt commented 8 years ago

@cpojer: I'm unsure if jumping in here 5 months later is the way to go but I couldn't find any other conversations about this.

Jumping off from your suggestion above, I've done this to mock out one function from another in the same module:

jest.unmock('./someModule.js');
import someModule from './someModule.js';

it('function1 calls function 2', () => {
    someModule.function2 = jest.fn();

    someModule.function1(...);

    expect(someModule.function2).toHaveBeenCalledWith(...);
});

This works for the one test, but I haven't found a way to accomplish this in a way that's isolated to just the one it(...); block. As written above, it affects every test which makes it hard to test the real function2 in another test. Any tips?

cpojer commented 8 years ago

You can call .mockClear on the function in beforeEach or call jest.clearAllMocks() if you are using Jest 16.

lnhrdt commented 8 years ago

Hey @cpojer! I am using Jest 16. Neither jest.clearAllMocks() or someModule.function2.mockClear() work for me. They only work when the mock is an entire module, not a function of an imported module. In my project, the function remains mocked in subsequent tests. If this isn't expected I'll see if I can replicate in a small sample project and create a new issue. Good idea?

yanivefraim commented 7 years ago

@cpojer -

The right solution in your example is not to mock foo but to mock the higher-level API that foo is calling (such as XMLHttpRequest or the abstraction that you use instead).

I'm new to Jest and I'm struggling with a similar problem. I'm using axios, which under the hood uses XMLHttpRequest, and I do not want to mock axios, but to mock the actual XMLHttpRequest. It seems that I would have to implement its methods by myself, something like this. Is this the right approach?

Thanks!

cpojer commented 7 years ago

yeah, something like this should get you on the right path! :) Use jest.fn as a nicer API, though :D

sorahn commented 7 years ago

@cpojer regarding your comment here: https://github.com/facebook/jest/issues/936#issuecomment-214939935

How would you do that with ES2015?

// myModyle.js
export foo = (string) => "foo-" + string
export bar = (string2) => foo(string2)

// myModule.test.js
var module = require('./myModule');

// how do I mock foo here so this test passes?
expect(bar("hello")).toEqual("test-hello")
ainesophaur commented 7 years ago

For anyone who comes across this looking for a solution, the following seems to be working for me when exporting many const/functions in one file, and importing them in a file which I'm testing


function mockFunctions() {
  const original = require.requireActual('../myModule');
  return {
    ...original, //Pass down all the exported objects
    test: jest.fn(() => {console.log('I didnt call the original')}),
    someFnIWantToCurry: {console.log('I will curry the original') return jest.fn((...args) => original.someFnIWantToCurry(...args)}),
  }
jest.mock('../myModule', () => mockFunctions());
const storage = require.requireMock('../myModule');
`
huyph commented 7 years ago

@ainesophaur, not sure what I am doing wrong here. But it seems it doesn't work I am currently on jest 18.1 (and create-react-app 0.9.4)

...<codes from comment above>..

// Let's say the original myModule has a function doSmth() that calls test()
storage.doSmth();
expect(storage.test).toHaveBeenCalled();

Test will then fail with:

expect(jest.fn()).toHaveBeenCalled()
Expected mock function to have been called.
ainesophaur commented 7 years ago

@huyph you'd have to mock your doSmth method and your test method in order for jest to test whether it was called. If you can provide the snippet of your mocking code I can check what's wrong

huyph commented 7 years ago

@ainesophaur ...uhm. I thought your codes above are for mocking the test() method? this part: test: jest.fn(() => {console.log('I didnt call the original')}),

rantonmattei commented 7 years ago

@ainesophaur I tried your code as well. But it did not work for me. It never executes the mock function. So, the expectation is never met.

I think this is inherent to the way require works like stated above... I wish there was a solution for this.

@cpojer Is there anything new regarding partially mocking modules?

ainesophaur commented 7 years ago

@rantonmattei & @huyph I'd have to see a snippet of your mock definitions and the test you're running. You have to define your mock before the actual implementation file is required/imported. Its been a while since I've worked with JEST, but I do remember I eventually got it to mock everything I needed to, whether it was a node_modules library or a file in my app. I'm a bit short on time ATM, but here is some of the tests from a project I worked on using Jest.

Mocking a file from a dependency

Actual function definition in this example is done by react-native.. I'm mocking the file "react-native/Libraries/Utilities/dismissKeyboard.js"

This is a mock file under __mocks__/react-native/Libraries/Utilities/dismissKeyboard.js

function storeMockFunctions() {
  return jest.fn().mockImplementation(() => true);
}
jest.mock('react-native/Libraries/Utilities/dismissKeyboard', () => storeMockFunctions(), { virtual: true });
const dismissKeyboard = require('react-native/Libraries/Utilities/dismissKeyboard');
exports = module.exports = storeMockFunctions;

I can't find the test file I used for the above, but it was something like require the module, jest would find it in mocks and then I could do something like

expect(dismissKeyboard.mock.calls).toHaveLength(1);

Mocking a file you control Actual function definition

export const setMerchantStores = (stores) => storage.set('stores', stores);

Test file with mock

const { storeListEpic, offerListEpic } = require('../merchant');

function storeMockFunctions() {
  const original = require.requireActual('../../common/Storage');
  return {
    ...original,
    setMerchantStores: jest.fn((...args) => original.setMerchantStores(...args)),
    setMerchantOffers: jest.fn((...args) => original.setMerchantOffers(...args)),
  };
}
jest.mock('../../common/Storage', () => storeMockFunctions());
import * as storage from '../../common/Storage';

afterEach(() => {
  storage.setMerchantStores.mockClear();
});

it('handle storeListEpic type STORE_LIST_REQUEST -> STORE_LIST_SUCCESS', async () => {
  const scope = nock('http://url')
  .get('/api/merchant/me/stores')
  .reply(200, storeData);
  const result = await storeListEpic(ActionsObservable.of(listStores())).toPromise();
  expect(storage.setMerchantStores.mock.calls).toHaveLength(1);
  expect(await storage.getMerchantStores()).toEqual({ ids: storesApiData.result, list: storesApiData.entities.store});
});
huyph commented 7 years ago

thanks for sharing @ainesophaur. I still can't get it to work with jest 18.1. Here are my codes:

it('should save session correctly', () => {

  function mockFunctions() {
    const original = require.requireActual('./session');
    return {
      ...original,
      restartCheckExpiryDateTimeout: jest.fn((() => {
        console.log('I didn\'t call the original');
      })),
    }
  }

  jest.mock('./session', () => mockFunctions());
  const mockSession = require('./session');

  // NOTE: saveSession() will call the original restartCheckExpiryDateTimeout() instead of my
  // mock one. However, mockSession.restartCheckExpiryDateTimeout() does call the mock one
  mockSession.saveSession('', getTomorrowDate(), 'AUTH');

  // mockSession.restartCheckExpiryDateTimeout(); // does print out "I didn't call the original"

  expect(mockSession.restartCheckExpiryDateTimeout).toHaveBeenCalled();
});

In session.js

export function saveSession(sessionId, sessionExpiryDate, authToken) {
  ....
  restartCheckExpiryDateTimeout(sessionExpiryDate);
  ...
}
....

export function restartCheckExpiryDateTimeout(...) {
....
}

I can't find a way to resolve this issue. Can I reopen this please? @cpojer

ainesophaur commented 7 years ago

@huyph the way you're doing the export saveSession is going to call the locally defined restartCheckExpiryDateTimeout instead of going through the module and calling module.restartCheckExpiryDateTimeout -- thus your mocked module.restartCheckExpiryDateTimeout wont be detected by saveSession as saveSession is calling the actual defined restartCheckExpiryDateTimeout function.

I'd assign saveSession to a const and then do saveSession.restartCheckExpiryDateTimeout = () => {...logic}. .then from within saveSession.saveSession, call saveSession.restartCheckExpiryDateTimeout instead of just restartCheckExpiryDateTimeout. Export your new const instead of the actual function saveSession which then defines your methods. Then when you call your someVar.saveSession() it'll then internally call saveSession.restartCheckExpiryDateTimeout() which is now mocked.

huyph commented 7 years ago

I should have added that restartCheckExpiryDateTimeout() is an exported function. Not a locally defined function within saveSession()... (Updated my comment above). In that case, I think module.saveSession() should call the right module.restartCheckExpiryDateTimeout()which is mocked.

But I will give what you suggest above a go though. Moving both saveSession() and restartCheckExpiryDateTimeout() to another const. Thanks

ainesophaur commented 7 years ago

I understand it's not defined in the scope of saveSession. saveSession is calling the sibling method in the parent scope. I ran into this many times and what I suggested worked for it

On May 8, 2017 8:38 PM, "Huy Pham" notifications@github.com wrote:

I should have added that restartCheckExpiryDateTimeout() is an exported function. Not a locally defined function within saveSession()...

I will give what you suggest above a go though. Thanks

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/facebook/jest/issues/936#issuecomment-300029003, or mute the thread https://github.com/notifications/unsubscribe-auth/AEeBdsmpOOmzvcUHB3D_-Z7MChIzt10Pks5r37WYgaJpZM4IPGAH .

huyph commented 7 years ago

Just tried that..I found that:

This does NOT work: (i.e the original restartCheckExpiryDateTimeout() is still called)

export session = {
   saveSession: () => {
      session.restartCheckExpiryDateTimeout();
   },
   restartCheckExpiryDateTimeout: () => {},
}

This works: (i.e the mock restartCheckExpiryDateTimeout() is called instead). The difference is the use of function() instead of arrow form, and the use of this. instead of session.

export session = {
   saveSession: function() {
      this.restartCheckExpiryDateTimeout();
   },
   restartCheckExpiryDateTimeout: () => {},
}

It could be a problem with jest transpiling these codes ....

ainesophaur commented 7 years ago

Try to export them as a class object instead of pojo. I believe the transpiler does hoist the variables differently. We will get to a working test, I promise.. Its been about half a year since I was on the project that used jest but I remember this problem well and I remember eventually finding a solution.

On May 9, 2017 12:53 AM, "Huy Pham" notifications@github.com wrote:

Just tried that..I found that:

This does NOT work: (i.e the original restartCheckExpiryDateTimeout() is still get called)

export session = { saveSession: () => { session.restartCheckExpiryDateTimeout(); }, restartCheckExpiryDateTimeout: () => {}, }

This does NOT work: (i.e the original restartCheckExpiryDateTimeout() is still get called)

export session = { saveSession: function() { this.restartCheckExpiryDateTimeout(); }, restartCheckExpiryDateTimeout: () => {}, }

It could be a problem with jest transpiling these codes ....

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/facebook/jest/issues/936#issuecomment-300060975, or mute the thread https://github.com/notifications/unsubscribe-auth/AEeBdrRQExycPYiGtvm7qYi5G87w6b6Oks5r3_FlgaJpZM4IPGAH .

mrdulin commented 7 years ago

@sorahn same issue. es6 + babel , How to mock? @cpojer Is that means es6 + babel , export const function xx() {} , export many function , Jest has no way to mock a function in a module(file) called by other function in the same module(file)? I test it, it seems I am correct. Just only for commonjs pattern, Jest can mock the function successfully, Like your example.

mrdulin commented 7 years ago

@ainesophaur not working.

module:

export const getMessage = (num: number): string => {
  return `Her name is ${genName(num)}`;
};

export function genName(num: number): string {
  return 'novaline';
}

test:

function mockFunctions() {
  const original = require.requireActual('../moduleA');
  return {
    ...original,
    genName: jest.fn(() => 'emilie')
  }
}
jest.mock('../moduleA', () => mockFunctions());
const moduleA = require('../moduleA');

describe('mock function', () => {

  it('t-0', () => {
    expect(jest.isMockFunction(moduleA.genName)).toBeTruthy();
  })

  it('t-1', () => {

    expect(moduleA.genName(1)).toBe('emilie');
    expect(moduleA.genName).toHaveBeenCalled();
    expect(moduleA.genName.mock.calls.length).toBe(1);
    expect(moduleA.getMessage(1)).toBe('Her name is emilie');
    expect(moduleA.genName.mock.calls.length).toBe(2);

  });

});

test result:

FAIL  jest-examples/__test__/mock-function-0.spec.ts
  ● mock function › t-1

    expect(received).toBe(expected)

    Expected value to be (using ===):
      "Her name is emilie"
    Received:
      "Her name is novaline"

      at Object.it (jest-examples/__test__/mock-function-0.spec.ts:22:35)
      at Promise.resolve.then.el (node_modules/p-map/index.js:42:16)

  mock function
    ✓ t-0 (1ms)
    ✕ t-1 (22ms)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        0.215s, estimated 1s
ainesophaur commented 7 years ago

Look at my last few comments above. Specifically the last one. Your exported methods are calling the locally scoped sibling method vs the actual exported method (which is where your mock is)

On May 31, 2017 2:00 AM, "novaline" notifications@github.com wrote:

@ainesophaur https://github.com/ainesophaur not working.

module:

export const getMessage = (num: number): string => { return Her name is ${genName(num)}; }; export function genName(num: number): string { return 'novaline'; }

test:

function mockFunctions() { const original = require.requireActual('../moduleA'); return { ...original, genName: jest.fn(() => 'emilie') } }jest.mock('../moduleA', () => mockFunctions());const moduleA = require('../moduleA'); describe('mock function', () => {

it('t-0', () => { expect(jest.isMockFunction(moduleA.genName)).toBeTruthy(); })

it('t-1', () => {

expect(moduleA.genName(1)).toBe('emilie');
expect(moduleA.genName).toHaveBeenCalled();
expect(moduleA.genName.mock.calls.length).toBe(1);
expect(moduleA.getMessage(1)).toBe('Her name is emilie');
expect(moduleA.genName.mock.calls.length).toBe(2);

});

});

test result:

FAIL jest-examples/test/mock-function-0.spec.ts ● mock function › t-1

expect(received).toBe(expected)

Expected value to be (using ===):
  "Her name is emilie"
Received:
  "Her name is novaline"

  at Object.it (jest-examples/__test__/mock-function-0.spec.ts:22:35)
  at Promise.resolve.then.el (node_modules/p-map/index.js:42:16)

mock function ✓ t-0 (1ms) ✕ t-1 (22ms)

Test Suites: 1 failed, 1 total Tests: 1 failed, 1 passed, 2 total Snapshots: 0 total Time: 0.215s, estimated 1s

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/facebook/jest/issues/936#issuecomment-305091749, or mute the thread https://github.com/notifications/unsubscribe-auth/AEeBdv6SafXlTtKo3DNeFWhbL6gV9l0Gks5r_QHjgaJpZM4IPGAH .

huyph commented 7 years ago

@ainesophaur : I attempted export class Session { }. And it doesn't work for me.

The only approach that works for me is in my comment above: where function syntax is used instead of arrow () =>. Here:

export const session = {
   saveSession: function() {
      this.restartCheckExpiryDateTimeout();
   },
   restartCheckExpiryDateTimeout: () => {},
}

This is on Jest 20.0.3

develerltd commented 7 years ago

What I do is create a const wrapper for the functions, and then export that wrapper (such as export const fns). Then inside the module use fns.functionName, and then I can jest.fn() the fns.functionName function

kailashyogeshwar85 commented 6 years ago

When we write mock function of a user-defined module which is written in typescript and when we call the mock function is the original function covered in coverage report because we are calling the mocked version of the function.

RealSilo commented 6 years ago

I have 2 functions that were originally imported in the tests as import { getCurrentDate, getStartEndDatesForTimeFrame } from ./../_helpers/date'; As you see getStartEndDatesForTimeFrame is dependent on getCurrentDate. With the following setup the getCurrentDate test works well and uses the mocked version. On the other hand for some reason the getStartEndDatesForTimeFrame test doesn't use the mocked getCurrentDate but the original implementation so my test fails. I have tried many different setups (like Date.now = jest.fn(() => "2017-11-16T20:33:09.071Z"); but couldn't make it work. Any ideas?

export const getCurrentDate = () => new Date();
export const getStartEndDatesForTimeFrame = (timeFrame) => {
  ...
  const todayDate = getCurrentDate();
  ...
  switch (timeframe) {
    case TimeFrames.TODAY:
      console.log(todayDate); // this always prints the real value in tests instead of the mocked one
      start = new Date(todayDate.getFullYear(), todayDate.getMonth(), todayDate.getDate(), 0, 0, 0);
      end = new Date(
        todayDate.getFullYear(),
        todayDate.getMonth(),
        todayDate.getDate(), 23, 59, 59,
      );
      break;
  ...
  return { start: start.toISOString(), end: end.toISOString() }
};
function mockFunctions() {
  const original = require.requireActual('../../_helpers/date');
  return {
    ...original,
    getCurrentDate: jest.fn(() => '2017-11-16T20:33:09.071Z'),
  }
}
jest.mock('../../_helpers/date', () => mockFunctions());
const dateModule = require.requireMock('../../_helpers/date');

describe('getCurrentDate', () => {
  it('returns the mocked date', () => {
    expect(dateModule.getCurrentDate()).
      toBe('2017-11-16T20:33:09.071Z'); // this works well and returns the mocked value
  });
});

describe('getStartEndDatesForTimeFrame', () => {
  it('returns the start and end dates for today', () => {
    expect(dateModule.getStartEndDatesForTimeFrame('today')).toEqual(
      { 'start': '2017-11-15T23:00:00.000Z', 'end': '2017-11-16T22:59:59.000Z' }
    ); // this one uses the original getCurrentDate instead of the mocked one :(
  });
});

So the getStartEndDatesForTimeFrame fails as it uses the current time not the mocked one.

miluoshi commented 6 years ago

I have managed to make it work by following a suggestion by @ainesophaur - by exporting all functions within an object and calling these exported object's methods instead of local scoped sibling methods:

// imageModel.js
const model = {
  checkIfImageExists,
  getImageUrl,
  generateImagePreview
}
export default model

async function checkIfImageExists(...) {}
async function getImageUrl() {}
async function generateImagePreview() {
  // I am calling it as `model` object's method here, not as a locally scoped function
  return model.getImageUrl(...)
}

// imageModel.test.js
import imageModel from './imageModel'

test('should mock getImageUrl called within the same file', async () => {
  imageModel.getImageUrl = jest.fn().mockReturnValueOnce(Promise.resolve())

  await imageModel.generateImagePreview()

  expect(imageModel.getImageUrl).toBeCalled()
})
Rdlenke commented 6 years ago

@miluoshi That's the only way I was able to do it too. Is there any loss of performance or something like that when we use this method? It seems "wrong "to change the code so you can test it.

BrennerSpear commented 6 years ago

I would really like a way to just write: jest.mock('src/folder/file.func, () => {return 'whatever i want'})

key piece here being the .func

mickr commented 6 years ago

@miluoshi @Rdlenke if your code consists of named exports you can also import * as model and then overwrite model.generateImagePreview = jest.fn(() => Promise.resolve);

SimenB commented 6 years ago

How would you test that with sinon? Like mentioned before (see https://github.com/facebook/jest/issues/936#issuecomment-214939935), the way ESM works make it impossible to mock func2 within func1, so I wouldn't necessarily call it basic.

develerltd commented 6 years ago

Maybe a babel mod could be written that reads in any "testImport" functions and rewrites the code to export the functions in the module prior to the test running?

On Mon, Dec 18, 2017 at 5:00 PM, Jim Moody notifications@github.com wrote:

You're right @SimenB https://github.com/simenb, I had changed something in my test in between switching to Sinon which mae it look like it passed. When I reverted that, it is still not working. I guess it's not a problem that has been solved.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/facebook/jest/issues/936#issuecomment-352488400, or mute the thread https://github.com/notifications/unsubscribe-auth/AQRY9a5-s2_bjCWKNw5WiAJW-JeBf8W3ks5tBpoygaJpZM4IPGAH .

--

Darren Cresswell Contract Developer | Develer Limited E-mail: darren@develer.co.uk Phone: Website: http://www.develer.co.uk

Please consider the environment before printing this email WARNING: Computer viruses can be transmitted via email. The recipient should check this email and any attachments for the presence of viruses. Develer Limited accepts no liability for any damage caused by any virus transmitted by this email. E-mail transmission cannot be guaranteed to be secure or error-free as information could be intercepted, corrupted, lost, destroyed, arrive late or incomplete, or contain viruses. The sender therefore does not accept liability for any errors or omissions in the contents of this message, which arise as a result of e-mail transmission.

WARNING: Although Develer Limited has taken reasonable precautions to ensure no viruses are present in this email, the company cannot accept responsibility for any loss or damage arising from the use of this email or attachments.

Develer Limited is a limited company registered in England and Wales. | Company Registration No. 09817616 | Registered Offices: SUITE 1 SECOND FLOOR EVERDENE HOUSE, DEANSLEIGH ROAD, BOURNEMOUTH, UNITED KINGDOM, BH7 7DU

annthurium commented 6 years ago

thanks @ainesophaur for the workaround.

In case anyone finds a non async working example useful, here's mine:

//reportError.js
const functions = {};

functions._reportToServer = (error, auxData) => {
  // do some stuff
};

functions.report = (error, auxData = {}) => {
  if (shouldReportToServer()) {
    functions._reportToServer(error, auxData);
  }
};
export default functions;

// reportError.jest.js
import reportError from 'app/common/redux/lib/reportError';
reportError._reportToServer = jest.fn();

describe('test reportError', () => {
  it('reports ERROR to server as as error', () => {
   reportError.report(new Error('fml'), {});
    expect(reportError._reportToServer).toHaveBeenCalledTimes(1);
  });
});
dinvlad commented 6 years ago

@jim-moody If I understood the issue correctly, this should work for your example:

const spyOnExampleFunc2 = jest.spyOn(example, 'func2');
example.func1();
expect(spyOnExampleFunc2).toBeCalled();

(this only works if the functions are exported as const's, like in your example)

iampeterbanjo commented 6 years ago

@dinvlad my hero!

iampeterbanjo commented 6 years ago

Following from @dinvlad 's answer, I think that adding, showing by example or linking the following mock related docs on the jest object page to the Mock functions page might be an improvement to the jest docs on mocking:

My use case is that as a new user of jest I'm migrating some mocha + sinon.js code to jest. I already had spies and expectations so I thought it would be easy. But after reading this thread and reading the jest docs on Mock functions I was getting the impression that using jest this way might need a rewrite of my tests or detailed understanding of ESM or Babel ... or other confusion.

Thanks for Jest - it's making my tests easier to write/understand and faster to execute. :)

SimenB commented 6 years ago

PR clarifying the docs is very welcome! 🙂

greypants commented 6 years ago

To mock only specific modules with ES module syntax, you can use require.requireActual to restore the original modules, then overwrite the one you want to mock:

import { foo } from './example';

jest.mock('./example', () => (
  ...require.requireActual('./example'),
  foo: jest.fn()
));

test('foo should be a mock function', () => {
  expect(foo('mocked!')).toHaveBeenCalledWith('mocked!');
});

Feels inverted, but it's the simplest way I've come across. Hat tip to @joshjg.

WangHansen commented 6 years ago

I am lost somewhere in the long discussion, I just had a question, is there anyway to testing if the actual implementation of the function is called?

From what I understand, if I need to use jest.fn() it will override the original function, but if I don't use it, the console would give me error saying it must be a jest.fn() function or a spy

I am trying to test a middleware where the request will be passed along, so if I mock it, all the logic will be lost and the data won't be passed to next middleware. If I don't mock it, by importing it, is there anyway I can test this function has been called?

SimenB commented 6 years ago

You can use jest.spyOn, maybe? By default it calls the underlying function

WangHansen commented 6 years ago

Thanks for the help, I tried, but the test suggests that it was never called even though it was called because I put the console.log and it did print

test file

import errorHandler from '../../controller/errorHandler'

describe('auth test', () => {
  describe('test error: ', () => {
    const test1 = jest.spyOn(errorHandler, 'handleClientError')
    test('should return 400', (done) => {
      request(app)
      .post('/auth/error')
      .then((res) => {
        expect(res.statusCode).toBe(400)
        expect(test1).toBeCalled()
        done()
      })
    })

errorHandler

module.exports = {
  handleClientError () {
    console.log('err')
  }
}

console

console.log src/controller/errorHandler.js:10
      err

  ● auth test › test error:  ›  should return 400

    expect(jest.fn()).toBeCalled()

    Expected mock function to have been called.

      18 |         expect(res.statusCode).toBe(400)
    > 19 |         expect(test1).toBeCalled()
      20 |         done()
      21 |       })
      22 |     })
SimenB commented 6 years ago

Is the function called handleClientError or logError?

iampeterbanjo commented 6 years ago

@WangHansen From your example your code should be expect(errorHandler.handleClientError).toBeCalled() // > true

dinvlad commented 6 years ago

@WangHansen could you add .mockImplementation() to your jest.spyOn()? As someone coming from Jasmine, I found this tip crucial to achieve the same functionality as Jasmine's spies. E.g.

const mockModuleFunction = jest
  .spyOn(module, 'function')
  .mockImplementation(() => 'hello');
...
expect(mockModuleFunction.mock).toBeCalled();

If you don't use mockImplementation(), then jest.spyOn() produces an object that is not a mock (afaiu) and it actually defers to the native implementation. If you do have to keep native implementation, maybe it's worth using

const moduleFunction = module.function;
jest.spyOn(module, 'function').mockImplementation(moduleFunction);
...

Not sure this is necessary, but fairly sure it should work.