Theodo apps

Fix jest mock 'Cannot access before initialization' error

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

++pre>++code data-line-start="6" data-line-end="24">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
);
++/code>++/pre>

🔴 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:

++pre>++code data-line-start="31" data-line-end="39">import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
 return jest.fn().mockImplementation(() => {
   return {playSoundFile: mockPlaySoundFile};
 });
});
++/code>++/pre>

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 ++code>mock++/code>.
  • What happens is that when doing the hoisting for ++code>jest.mock++/code>, 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 ++code>mock++/code> 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:

++table border="1" cellpadding="4" style="width: 100%; border-color: #ff0201; border-style: solid; border-collapse: collapse; table-layout: fixed; height: 345px;">++tbody>++tr style="height: 33px;">++td style="width: 100%; background-color: #eeeeee; height: 33px;">Fails++/td>++/tr>++tr style="height: 312px;">++td style="width: 100%; height: 312px;">++pre>++code data-line-start="63" data-line-end="69">const useDispatchMock = () => jest.fn();

jest.mock('react-redux', () => ({
 useDispatch: useDispatchMock, // ❌ BREAKS, useDispatchMock needed while hoisting.
}));
++/code>++/pre>Here, ++code>useDispatchMock++/code> will be needed when doing the actual mocking (before ++code data-line-start="63" data-line-end="69">useDispatchMock++/code> initialization).

Jest needs to return ++code>useDispatchMock++/code> when you ask for ++code>useDispatch++/code>.

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

++/td>++/tr>++/tbody>++/table>

 

++table border="1" cellpadding="4" style="width: 100%; border-color: #00FF03; border-style: solid; border-collapse: collapse; table-layout: fixed;">++tbody>++tr>++td style="width: 100%; background-color: #eeeeee;">Works++/td>++/tr>++tr>++td style="width: 100%;">++pre>++code data-line-start="77" data-line-end="83">const dispatchMock = jest.fn();

jest.mock('react-redux', () => ({
 useDispatch: () => dispatchMock, // ✅ WORKS, dispatchMock needed only on execution
}));
++/code>++/pre>Here, ++code>dispatchMock++/code> is not necessary when doing the actual mocking. It's only necessary when ++code data-line-start="77" data-line-end="83">useDispatch++/code> is called, after ++code data-line-start="77" data-line-end="83">dispatchMock ++/code>++code data-line-start="77" data-line-end="83">++/code>initialization.

Jest needs to return a ++code>function++/code> when you ask for ++code>useDispatch++/code>

➡ this ++code>function++/code> will later return ++code>dispatchMock++/code> when you actually run it.

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

➡ the ++code>function++/code> is not run right now (mocking phase)

++/td>++/tr>++tr>++td style="width: 100%;">

In Jest example:

++pre>++code data-line-start="97" data-line-end="105">import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
 return jest.fn().mockImplementation(() => {
   return {playSoundFile: mockPlaySoundFile};
 });
});
++/code>++/pre>

++code>mockPlaySoundFile++/code> is not necessary when doing the actual mocking.

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

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

➡ jest doesn't need to know what ++code>mockPlaySoundFile++/code> 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.

++/td>++/tr>++/tbody>++/table>

 

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.

++pre>++code data-line-start="123" data-line-end="132">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
}));
++/code>++/pre>

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

++pre>++code data-line-start="136" data-line-end="154">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
);++/code>++/pre>

Développeur mobile ?

Rejoins nos équipes