Skip to content

Instantly share code, notes, and snippets.

@gadflying
Last active June 25, 2024 10:37
Show Gist options
  • Save gadflying/5f2bf3c53370a0ceda5108816aedd298 to your computer and use it in GitHub Desktop.
Save gadflying/5f2bf3c53370a0ceda5108816aedd298 to your computer and use it in GitHub Desktop.
[How to mock imported named function in Jest when module is unmocked] #jest #reactTestLib
// 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')
  })
})
// 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.

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:

Bypassing module mocks

Jest allows you to mock out whole modules in your tests, which can be useful for testing if your code is calling functions from that module correctly. However, sometimes you may want to use parts of a mocked module in your test file, in which case you want to access the original implementation, rather than a mocked version.

Consider writing a test case for this createUser function:

// createUser.js
import fetch from 'node-fetch';

export const createUser = async () => {
  const response = await fetch('http://website.com/users', {method: 'POST'});
  const userId = await response.text();
  return userId;
};

Copy Your test will want to mock the fetch function so that we can be sure that it gets called without actually making the network request. However, you'll also need to mock the return value of fetch with a Response (wrapped in a Promise), as our function uses it to grab the created user's ID. So you might initially try writing a test like this:

jest.mock('node-fetch');

import fetch, {Response} from 'node-fetch';
import {createUser} from './createUser';

test('createUser calls fetch with the right args and returns the user id', async () => {
  fetch.mockReturnValue(Promise.resolve(new Response('4')));

  const userId = await createUser();

  expect(fetch).toHaveBeenCalledTimes(1);
  expect(fetch).toHaveBeenCalledWith('http://website.com/users', {
    method: 'POST',
  });
  expect(userId).toBe('4');
});

However, if you ran that test you would find that the createUser function would fail, throwing the error: TypeError: response.text is not a function. This is because the Response class you've imported from node-fetch has been mocked (due to the jest.mock call at the top of the test file) so it no longer behaves the way it should.

To get around problems like this, Jest provides the jest.requireActual helper. To make the above test work, make the following change to the imports in the test file:

// BEFORE
jest.mock('node-fetch');
import fetch, {Response} from 'node-fetch';

Copy
// AFTER
jest.mock('node-fetch');
import fetch from 'node-fetch';
const {Response} = jest.requireActual('node-fetch');

Copy This allows your test file to import the actual Response object from node-fetch, rather than a mocked version. This means the test will now pass correctly.

如何mock 一个module EventTracker

//EventTracker.js
export const trackEventName = (
  eventType,
  eventTitle
) => `Track Event - ${eventType} - ${eventTitle}`;

export function trackEvent(eventName){
  //do something not important 
}

And then we have a function that we want to be tested:

//ClickBoxTracking.js
import React from 'react';
import { trackEventName, trackEvent } from '../../EventTracker';
import {ClickBox} from "../ClickBox"
export const ClickBoxWithTracking = ({
  trackingTitle,
  ...props
}) => {
  const tracking = () => {
    trackEvent(trackEventName('ClickBox', trackingTitle));
  };

  return (<ClickBox tracking={tracking} {...props} />);
};

Finally this is how we test it :

最终要地方

import { trackEvent } from '../../EventTracker';//1 先引入

jest.mock('../../EventTracker', () => {
  const actualTracker = jest.requireActual('../../EventTracker');
  return {
    ...actualTracker,
    trackEvent: jest.fn(),
  };
});
 const MockTrackEvent = trackEvent;
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { ClickBoxWithTracking } from './ClickBoxTracking';
import { trackEvent } from '../../EventTracker';//1 先引入

jest.mock('../../EventTracker', () => {
  const actualTracker = jest.requireActual('../../EventTracker');
  return {
    ...actualTracker,
    trackEvent: jest.fn(),
  };
});

describe('ClickBoxWithTracking', () => {
  const MockTrackEvent = trackEvent;
  let wrapper;
  beforeEach(() => {
    wrapper = mount(<ClickBoxWithTracking trackingTitle="Current tick option">Current tick option...</ClickBoxWithTracking>);
  });

  it('should call trackEvent when the clickBox is ticked', () => {
    //do something to simulate the click 
  expect(MockTrackEvent).toHaveBeenCalledWith('ClickBox - Current tick option');
  });

});

Notice when we do the following, we are returning { trackEventName, trackEvent } and then trackEvent: jest.fn() overwrite the trackeEvent returned from the module.

jest.mock('../../EventTracker', () => {
  const actualTracker = jest.requireActual('../../EventTracker');
  return {
    ...actualTracker,
    trackEvent: jest.fn(),
  };
});

The reason we want to do this is because when we do the mounting: mount(Current tick option...); There is this method get triggered inside the component:

const tracking = () => {
    trackEvent(trackEventName('ClickBox', trackingTitle));
  };

We still want to call trackEventName but just want to mock trackEvent . So we require the real module thereby requiring the real trackEventName and overwrite the latter with the mocked trackEvent . Of course you can mock trackEventName as well. But in this test, we don’t want to add another layer of complexity, and would like to actually test if the trackEventName can be passed in to trackEvent and rendered by ClickBoxWithTracking . You can even more explicitly requiring the real trackEventName like this after you require the module.

jest.mock('../../EventTracker', () => {
  const actualTracker = jest.requireActual('../../EventTracker');
  return {
    ...actualTracker,
    trackEvent: jest.fn(),
  };
});
const { trackEventName } = require('../../EventTracker');
```js
//do something with trackingEventName
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment