Theodo apps

Fix jest mock 'Cannot access before initialization' error

Have you ever run into this error while creating a variable before your jest.mock:

const mockIsLoggedIn = jest.fn();
jest.mock('./utils', () => ({
 ...
 isLoggedIn: mockIsLoggedIn,
}));
it('does ... when user is logged in', () => {
   mockIsLoggedIn.mockReturnValue(true);
   ... // test while logged in
);
it('does ... when user is logged out', () => {
   mockIsLoggedIn.mockReturnValue(false);
   ... // test while logged out
);

🔴 Test suite failed to run: ReferenceError: Cannot access 'mockIsLoggedIn' before initialization

 

Maybe you don't even understand why because the official docs displays this working example:

import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
 return jest.fn().mockImplementation(() => {
   return {playSoundFile: mockPlaySoundFile};
 });
});

They even mention:

"since calls to jest.mock() are hoisted to the top of the file, it's not possible to first define a variable and then use it in the factory. An exception is made for variables that start with the word 'mock'."

⚠️ But the very next sentence, "It's up to you to guarantee that they will be initialized on time!" is what's crucial here.

 

The in-depth explanation is here 👇

  • JEST hoist the jest.mock block (? move it to the top of the file before running it).
  • JEST doesn't hoist variables that start with mock.
  • What happens is that when doing the hoisting for jest.mock, the babel plugin checks variables that are being used in the mock factory to ensure that everything being used will be within scope after hoisting.
  • So if it finds a variable (if "mockPlaySoundFile" didn't start with "mock") it'll throw; it's effectively doing a little bit of what TypeScript does in general, to be helpful given that this hoisting is happening in the background & so not obvious. (warning ? this only happens if you use babel).
  • However, as an advanced escape hatch, the babel plugin allows variables that start with mock as a way to tell jest that you know what you're doing.

 

Why does my code throw and not the one in the docs?

Let's take a simpler example:

Fails
const useDispatchMock = () => jest.fn();
jest.mock('react-redux', () => ({
 useDispatch: useDispatchMock, // ❌ BREAKS, useDispatchMock needed while hoisting.
}));
Here, useDispatchMock will be needed when doing the actual mocking (before useDispatchMock initialization).

Jest needs to return useDispatchMock when you ask for useDispatch.

➡ it needs to know what it is right now (mocking phase)

 

Works
const dispatchMock = jest.fn();
jest.mock('react-redux', () => ({
 useDispatch: () => dispatchMock, // ✅ WORKS, dispatchMock needed only on execution
}));
Here, dispatchMock is not necessary when doing the actual mocking. It's only necessary when useDispatch is called, after dispatchMock initialization.

Jest needs to return a function when you ask for useDispatch

➡ this function will later return dispatchMock when you actually run it.

➡ jest doesn't need to know what dispatchMock is as long as you haven't run the function

➡ the function is not run right now (mocking phase)

In Jest example:

import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
 return jest.fn().mockImplementation(() => {
   return {playSoundFile: mockPlaySoundFile};
 });
});

mockPlaySoundFile is not necessary when doing the actual mocking.

Jest needs to return a jest.fn() when you ask for './sound-player'.

➡ this jest.fn() will later return mockPlaySoundFile (here it's inside an object returned because of the mockImplementation) when you actually run it.

➡ jest doesn't need to know what mockPlaySoundFile is as long as you haven't run the './sound-player'.

➡ this is exactly like the above example but with a complex syntax

➡ maybe this article can help you get an even better grasp of what's happening under the hood.

 

The general solution

Do like in the jest docs when you can. ➡ This works when what you want control over isn't returned directly by the mock but rather by a function in the mock.

const myMock = jest.fn();
jest.mock('lib', () => ({
 useLogin: () => myMock(), // ✅ WORKS, myMock needed only on execution
 logout: myMock, // ❌ BREAKS, myMock needed while mocking
 complexHook: () => ({ callback: myMock }), // ✅ WORKS, myMock needed only on execution
 complexSyntaxHook: jest.fn().mockImplementation(() => ({ callback: myMock })), // ✅ WORKS, myMock needed only on execution
}));

And if you cannot do that (like in my 1st example & in the above example that breaks ❌) you can opt for the following solution:

import { isLoggedIn } from './utils';
jest.mock('./utils', () => ({
 ...
 isLoggedIn: jest.fn(),
}));
it('does ... when user is logged in', () => {
   (isLoggedIn as jest.Mock).mockReturnValue(true);    // 'as jest.Mock' so that typescript understands you're not using the real 'isLoggedIn' but a mocked one.
   ... // test while logged in
);
it('does ... when user is logged out', () => {
   (isLoggedIn as jest.Mock).mockReturnValue(false);
   ... // test while logged out
);

Développeur mobile ?

Rejoins nos équipes