0

I am having trouble with Jest not hoisting mock functions declared prefixed with 'mock' It's my understanding that this should work according to the jest docs

I have a redux actions that does something with another dependency. The result of call a method on the dependent module is then dispatched with another action.

How can I mock the implementation of resume in the dependent module AuthUtils. Calling the thunk throws an error because the resume method is undefined

Actions.js

import { setUser } from '../../src/actions/UserActions';
import AuthUtils from '../utils/AuthUtils'; //dependent es6 class
const auth = new AuthUtils(); 

export const resumeSession = () => async (dispatch, getState) => {
  try {
    const resumeResult = await auth.resume(); // wait for result
    dispatch(setUser(resumeResult)); //dispatch setUser with result
  } catch() {

  }
};

Actions.test.js:

import { resumeSession } from '../../src/actions/AuthActions';
import { setUser } from '../../src/actions/UserActions';

// auto mock UserActions
jest.mock('../../src/utils/UserActions');

// Mock resume method of AuthUtils using module factory param
// The mockResume here is undefined, but I expected because it begins with mock it would be hoisted along with the jest.mock call
// "An exception is made for variables that start with the word 'mock'." -- from the docks

const mockResume = jest.fn(() => Promise.resolve({ user: { things } }));
jest.mock('../../src/utils/AuthUtils', () => {
  return jest.fn().mockImplementation(() => {
    return { resume: mockResume };
  });
});

describe('resumeSession', () => {
  it('dispatches complete', async () => {
     const mockDispatch = jest.fn();
     const mockGetState = jest.fn();
     await resumeSession()(mockDispatch, mockGetState);
     expect(setUser).toHaveBeenCalledWith({ user: { things } });
     // Test blows up because AuthUtils#resume is not a function
  });
});

1 Answer 1

1

In this case, I'm 99% sure the problem is that you're mocking too late.

const auth = new AuthUtils(); is inline code in the module file. That means it is executed as soon as the file is imported.

Your test file runs code in the following order:

import { resumeSession } from '../../src/actions/AuthActions';
// this does:
//     import AuthUtils from '../utils/AuthUtils';
//     const auth = new AuthUtils(); 
import { setUser } from '../../src/actions/UserActions';

jest.mock('../../src/utils/UserActions');

const mockResume = jest.fn(() => Promise.resolve({ user: { things } }));
jest.mock('../../src/utils/AuthUtils', () => {
  return jest.fn().mockImplementation(() => {
    return { resume: mockResume };
  });
});
// too late, since the code from the *actual* AuthUtils has already been executed

This would work fine if auth was a local variable in your resumeSession function, like so:

export const resumeSession = () => async (dispatch, getState) => {
  const auth = new AuthUtils();

  try {
    const resumeResult = await auth.resume(); // wait for result
    dispatch(setUser(resumeResult)); //dispatch setUser with result
  } catch() {

  }
};

Because then the mock is set up before any code tries to use AuthUtils. But I assume you create auth outside the function for a reason.

If moving your instantiation of auth to inside your function is not an option, one possible solution is to instead move your mock and setup of AuthUtils and its resume function to before your import from AuthActions:

const mockResume = jest.fn(() => Promise.resolve({ user: { things } }));
jest.mock('../../src/utils/AuthUtils', () => {
  return jest.fn().mockImplementation(() => {
    return { resume: mockResume };
  });
});

import { resumeSession } from '../../src/actions/AuthActions';
import { setUser } from '../../src/actions/UserActions';

jest.mock('../../src/utils/UserActions');

If that doesn't work (or if you prefer not to have any code before your imports), another option is to export your auth variable so you can spy on the actual instance and mock its resume function:

import { auth, resumeSession } from '../../src/actions/AuthActions';

const mockResume = jest.fn(() => Promise.resolve({ user: { things } }));
jest.spyOn(auth, "resume").mockImplementation(mockResume);

This may have the side effect of keeping your mocked implementation around for other tests after this one is done, which you probably don't want. You can use Jest's lifecycle methods to avoid that and restore the original resume implementation when your tests are complete:

const mockResume = jest.fn(() => Promise.resolve({ user: { things } }));
const resumeSpy = jest.spyOn(auth, "resume");
resumeSpy.mockImplementation(mockResume);

describe('resumeSession', () => {
  afterAll(() => {
    resumeSpy.mockRestore();
  });

  it('dispatches complete', async () => {
     const mockDispatch = jest.fn();
     const mockGetState = jest.fn();
     await resumeSession()(mockDispatch, mockGetState);
     expect(setUser).toHaveBeenCalledWith({ user: { things } });
  });
});

Unrelated sidenote: Jest mock functions (and spies) have a handy function to mock Promise results, so you don't need to have a mock implementation that manually calls Promise.resolve() or Promise.reject(). I personally prefer using Jest's own functions:

const mockResume = jest.fn();
mockResume.mockResolvedValue({ user: { things } }));

If you use the spy approach, you can drop the mockResume function altogether:

const resumeSpy = jest.spyOn(auth, "resume");
resumeSpy.mockResolvedValue({ user: { things } }));

This is not related to the problem you're currently having, but I thought I'd throw it out there.

Sign up to request clarification or add additional context in comments.

1 Comment

Absolutely spot on, I moved the initialisation of the AuthUtils class into the resume method and low and behold everything worked as expected. I had it outside the methods because there's more actions and they just share the same instance, but there's no real downside to each initialising a fresh instance. Some really great pointers, really appreciate your answer Rick

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.