Cannot mock a module with jest, and test function calls

67,608

Solution 1

The problem

The reason you're getting that error has to do with how various operations are hoisted.

Even though in your original code you only import SearchContainer after assigning a value to mockSearch and calling jest's mock, the specs point out that: Before instantiating a module, all of the modules it requested must be available.

Therefore, at the time SearchContainer is imported, and in turn imports search , your mockSearch variable is still undefined.

One might find this strange, as it would also seem to imply search.js isn't mocked yet, and so mocking wouldn't work at all. Fortunately, (babel-)jest makes sure to hoist calls to mock and similar functions even higher than the imports, so that mocking will work.

Nevertheless, the assignment of mockSearch, which is referenced by the mock's function, will not be hoisted with the mock call. So, the order of relevant operations will be something like:

  1. Set a mock factory for ./search.js
  2. Import all dependencies, which will call the mock factory for a function to give the component
  3. Assign a value to mockSearch

When step 2 happens, the search function passed to the component will be undefined, and the assignment at step 3 is too late to change that.

Solution

If you create the mock function as part of the mock call (such that it'll be hoisted too), it'll have a valid value when it's imported by the component module, as your early example shows.

As you pointed out, the problem begins when you want to make the mocked function available in your tests. There is one obvious solution to this: separately import the module you've already mocked.

Since you now know jest mocking actually happens before imports, a trivial approach would be:

import { search } from './search.js'; // This will actually be the mock

jest.mock('./search.js', () => {
  return { search: jest.fn(() => mockPromise) };
});

[...]

beforeEach(() => {
  search.mockClear();
});

it('should call the search module', () => {
  [...]

  expect(search.mock.calls.length).toBe(1);
  expect(search.mock.calls[0]).toEqual(expectedArgs);
});

In fact, you might want to replace:

import { search } from './search.js';

With:

const { search } = require.requireMock('./search.js');

This shouldn't make any functional difference, but might make what you're doing a bit more explicit (and should help anyone using a type-checking system such as Flow, so it doesn't think you're trying to call mock functions on the original search).

Additional note

All of this is only strictly necessary if what you need to mock is the default export of a module itself. Otherwise (as @publicJorn points out), you can simply re-assign the specific relevant member in the tests, like so:

import * as search from './search.js';

beforeEach(() => {
  search.search = jest.fn(() => mockPromise);
});

Solution 2

In my case, I got this error because I failed to implement the mock correctly.

My failing code:

jest.mock('react-native-some-module', mockedModule);

When it should have been an arrow function...

jest.mock('react-native-some-module', () => mockedModule);

Share:
67,608
ghusse
Author by

ghusse

Updated on April 08, 2020

Comments

  • ghusse
    ghusse about 4 years

    I create a project using create-app-component, which configures a new app with build scripts (babel, webpack, jest).

    I wrote a React component that I'm trying to test. The component is requiring another javascript file, exposing a function.

    My search.js file

    export {
      search,
    }
    
    function search(){
      // does things
      return Promise.resolve('foo')
    }
    

    My react component:

    import React from 'react'
    import { search } from './search.js'
    import SearchResults from './SearchResults'
    
    export default SearchContainer {
      constructor(){
        this.state = {
          query: "hello world"
        }
      }
    
      componentDidMount(){
        search(this.state.query)
          .then(result => { this.setState({ result, })})
      }
    
      render() {
        return <SearchResults 
                result={this.state.result}
                />
      }
    }
    

    In my unit tests, I want to check that the method search was called with the correct arguments.

    My tests look something like that:

    import React from 'react';
    import { shallow } from 'enzyme';
    import should from 'should/as-function';
    
    import SearchResults from './SearchResults';
    
    let mockPromise;
    
    jest.mock('./search.js', () => {
      return { search: jest.fn(() => mockPromise)};
    });
    
    import SearchContainer from './SearchContainer';
    
    describe('<SearchContainer />', () => {
      it('should call the search module', () => {
        const result = { foo: 'bar' }
        mockPromise = Promise.resolve(result);
        const wrapper = shallow(<SearchContainer />);
    
        wrapper.instance().componentDidMount();
    
        mockPromise.then(() => {
          const searchResults = wrapper.find(SearchResults).first();
          should(searchResults.prop('result')).equal(result);
        })    
      })
    });
    

    I already had a hard time to figure out how to make jest.mock work, because it requires variables to be prefixed by mock.

    But if I want to test arguments to the method search, I need to make the mocked function available in my tests.

    If I transform the mocking part, to use a variable:

    const mockSearch = jest.fn(() => mockPromise)
    jest.mock('./search.js', () => {
      return { search: mockSearch};
    });
    

    I get this error:

    TypeError: (0 , _search.search) is not a function

    Whatever I try to have access to the jest.fn and test the arguments, I cannot make it work.

    What am I doing wrong?

  • ghusse
    ghusse over 7 years
    Thanks for this very detailed response. I'll definitively try your solution.
  • Tomty
    Tomty over 7 years
    While the previous solution should technically work (and I even eventually figured out why the var was necessary), I've now edited the answer to use a much less ugly one, IMO.
  • ghusse
    ghusse over 7 years
    Thanks for your help. With the second solution, I get an error cannot set property search of search. The first solution works: I need to import the lib and call jest once for all (not in the beforeEach trigger).
  • Tomty
    Tomty over 7 years
    @ghusse Glad to hear the first solution worked for you! As for the second one, not really sure why you'd get that error. If you find it important enough, feel free to attach (either as an update to this question or as a new one) an updated code sample with that solution along with the exact error, and I'll be happy to try to debug.
  • publicJorn
    publicJorn about 7 years
    @Tomty as for you additional note: you actually only have to change your test import: import * as search from './search.js' and then search.search = jest.fn(() => mockPromise). That way you can keep your actual code nice and clean import { search } from './search.js'. This is because you have to mock the function, but you can't directly mock an imported constant.
  • Tomty
    Tomty about 7 years
    @publicJorn Excellent point! I'll update the "Additional notes" section of my answer accordingly. Or, if you'd like to post this as a separate answer, just let me know and I'll remove it from mine.
  • swandog
    swandog about 6 years
    One other note...when testing a negative response make sure your page/component api call uses the myapi.getsomedata().then(success=>{},error =>{}); syntax not .then(success=>{}).catch(function(err){....
  • Kitson
    Kitson about 3 years
    For some reason whatever I tried in terms of mocking my module wouldn't work, but the solution in your "Additional note" was the one that got a working solution! thanks