ChatGPT解决这个技术问题 Extra ChatGPT

How to mock imported named function in Jest when module is unmocked

I have the following module I'm trying to test in Jest:

// myModule.js

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

export function testFn() {
  otherFn();

  // do other things
}

As shown above, it exports some named functions and importantly testFn uses otherFn.

In Jest when I'm writing my unit test for testFn, I want to mock the otherFn function because I don't want errors in otherFn to affect my unit test for testFn. My issue is that I'm not sure the best way to do that:

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

Any help/insight is appreciated.

I wouldn't do this. Mocking is generally not something you want to do anyway. And if you need to mock something (due to making server calls/etc.) then you should just extract otherFn into a separate module and mock that.
@kentcdodds, Could you clarify what you mean by "Mocking is generally not something you want to do anyway."? That seems to be a fairly broad (overly broad?) statement, as mocking is certainly something that is often used, presumably for (at least some) good reasons. So, are you perhaps referring to why mocking might not be good here, or do you really mean in general?
Often mocking is testing implementation details. Especially at this level it leads to tests that aren't really validating much more than the fact that your tests work (not that your code works).
I was about to call that first commenter an idiot before I realized who it was. Very confused by that comment, though. First off, mocking is essential to testing modern applications. Also, I have the same situation and it would make no sense to separate the functions into separate modules.
@kentcdodds So, if my component makes an api call, how can I not mock that to test the output of the render? I don't think mocking an api call is testing implementation details. For example, if my api call fills user data on a page, the output of my component is the page with the data, so I can't really test without it. Right? I'm still testing outputs with inputs (the userId prop, for example). I'm not testing whether the api call was made or not. Or am I completely misunderstanding?

P
Patrick

Use jest.requireActual() inside jest.mock()

jest.requireActual(moduleName) Returns the actual module instead of a mock, bypassing all checks on whether the module should receive a mock implementation or not.

Example

I prefer this concise usage where you require and spread within the returned object:

// myModule.test.js

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

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

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

This method is also referenced in Jest's Manual Mocks documentation (near the end of Examples):

To ensure that a manual mock and its real implementation stay in sync, it might be useful to require the real module using jest.requireActual(moduleName) in your manual mock and amending it with mock functions before exporting it.


Fwiw, you can make this even more concise by removing the return statement and wrapping the arrow function body in parentheses: eg. jest.mock('./myModule', () => ({ ...jest.requireActual('./myModule'), otherFn: () => {}}))
This worked great! This should be the accepted answer.
Not working for me: jest.mock('../../src/helpers', () => ({ ...jest.requireActual('../../src/helpers'), getCurrentMessage: (): { getFrom: () => string } => { return { getFrom: (): string => 'Test User <test.user@example.com>', }; }, })); The function I'm testing is still calling the actual getCurrentMessage.
...jest.requireActual didn't work for me properly because I have path-aliasing using babel.. Works either with ...require.requireActual or after removing aliasing from path
This answer actually doesn't answer the question. Provided solution doesn't call testFn that in turn should call a mock of otherFn. If you call testFn it will still use original otherFn. At least, it doesn't work for me in TypeScript. See the explaination.
B
Brian Adams

Looks like I'm late to this party, but yes, this is possible.

testFn just needs to call otherFn using the module.

If testFn uses the module to call otherFn then the module export for otherFn can be mocked and testFn will call the mock.

Here is a working example:

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

This is essentially an ES6 version of the approach used at Facebook and described by a Facebook dev in the middle of this post.
instead of importing myModule into itself, just call exports.otherFn()
@andrhamm exports doesn't exist in ES6. Calling exports.otherFn() works right now because ES6 is being compiled to an earlier module syntax, but it will break when ES6 is supported natively.
Having this exact problem right now and I'm sure I've encountered this problem before. I've had to remove a load of exports.<methodname> to help tree shaking along and it's broken many tests. I'll see if this has any effect, but it seems so hacky. I've come across this problem a number of times and like other answers have said, things like babel-plugin-rewire or even better, npmjs.com/package/rewiremock which I'm fairly sure can do the above too.
Is it possible to instead of mock a return value, mock a throw? Edit: you can, here is how stackoverflow.com/a/50656680/2548010
b
bobu
import m from '../myModule';

Does not works for me, I did use:

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

m.otherFn = jest.fn();

How would you restore otherFn's original functionality after the test so that it does not interfere with other testS?
I think you can configure jest to clear mocks after every test? From the docs: "The clearMocks configuration option is available to clear mocks automatically between tests.". You can set clearkMocks: true jest package.json config. facebook.github.io/jest/docs/en/mock-function-api.html
if this is such problem to change global state you can always store original functionality inside some kind of test variable and bring back it after test
const original; beforeAll(() = > { original = m.otherFn; m.otherFn = jest.fn(); }) afterAll(() => { m.otherFn = original; }) it should work, however I didn't test it
J
Jack Kinsey

I know this was asked a long time ago, but I just ran into this very situation and finally found a solution that would work. So I thought I'd share here.

For the module:

// myModule.js

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

export function testFn() {
  otherFn();

  // do other things
}

You can change to the following:

// myModule.js

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

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

  // do other things
}

exporting them as a constants instead of functions. I believe the issue has to do with hoisting in JavaScript and using const prevents that behaviour.

Then in your test you can have something like the following:

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

Your mocks should now work as you would normally expect.


I'm stuck with the same problem now and I'm wondering how would you mock otherFn if, instead of being a function, it would be an array? So testFn is reading an array that is also exported from the module and I'd like to mock that array in my tests to change the behaviour of testFn without mocking itself.
v
vutran

The transpiled code will not allow babel to retrieve the binding that otherFn() is referring to. If you use a function expession, you should be able to achieve mocking 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();

But as @kentcdodds mentioned in the previous comment, you probably would not want to mock otherFn(). Rather, just write a new spec for otherFn() and mock any necessary calls it is making.

So for example, if otherFn() is making an http request...

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

Here, you would want to mock http.get and update your assertions based on your mocked implementations.

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

what if otherFn and testFn is used by several other modules? would you need to set up the http mock in all the test files which uses (however deep the stack) those 2 modules? Also, if you already have a test for testFn, why not stub testFn directly instead of http in the modules that uses testFn?
so if otherFn is broken, will fail all tests that depends on that one. Also if otherFn has 5 ifs inside you might need to test that your testFn is working fine for all those sub cases. You will have so many more code paths to test now.
e
ezze

Basing on Brian Adams' answer this is how I was able to use the same approach in TypeScript. Moreover, using jest.doMock() it's possible to mock module functions only in some specific tests of a test file and provide an individual mock implementations for each of them.

src/module.ts

import * as module from './module';

function foo(): string {
  return `foo${module.bar()}`;
}

function bar(): string {
  return 'bar';
}

export { foo, bar };

test/module.test.ts

import { mockModulePartially } from './helpers';

import * as module from '../src/module';

const { foo } = module;

describe('test suite', () => {
  beforeEach(function() {
    jest.resetModules();
  });

  it('do not mock bar 1', async() => {
    expect(foo()).toEqual('foobar');
  });

  it('mock bar', async() => {
    mockModulePartially('../src/module', () => ({
      bar: jest.fn().mockImplementation(() => 'BAR')
    }));
    const module = await import('../src/module');
    const { foo } = module;
    expect(foo()).toEqual('fooBAR');
  });

  it('do not mock bar 2', async() => {
    expect(foo()).toEqual('foobar');
  });
});

test/helpers.ts

export function mockModulePartially(
  modulePath: string,
  mocksCreator: (originalModule: any) => Record<string, any>
): void {
  const testRelativePath = path.relative(path.dirname(expect.getState().testPath), __dirname);
  const fixedModulePath = path.relative(testRelativePath, modulePath);
  jest.doMock(fixedModulePath, () => {
    const originalModule = jest.requireActual(fixedModulePath);
    return { ...originalModule, ...mocksCreator(originalModule) };
  });
}

Mocking functions of a module is moved to helper function mockModulePartially located in a separate file so it can be used from different test files (which, in common, can be located in other directories). It relies on expect.getState().testPath to fix path to a module (modulePath) being mocked (make it relative to helpers.ts containing mockModulePartially). mocksCreator function passed as a second argument to mockModulePartially should return mocks of the module. This function receives originalModule and mock implementations can optionally rely on it.


d
demaroar

I solved my problem with a mix of the answers that I found here:

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

s
schrodinger's code

On top of the first answer here, you can use babel-plugin-rewire to mock imported named function too. You can check out the section superficially for named function rewiring.

One of the immediate benefits for your situation here is that you do not need to change how you call the other function from your function.


How to configure babel-plugin-rewire to work with node.js?