It took a few hours to understand how the jest.mock
works. I put the note about my learnings here.
Background
Normally, I write ReactNative code for my job and refactored the test codes last week. Then I tried to make a function to mock a React component in order to put the same process in a test utility file. It was like below.
export function mockComponent<T>(componentName: string, mockFuntion: (props: T) => void) {
const React = require('react');
const Component = class extends React.PureComponent {
render() {
const { children } = this.props;
mockFuntion(this.props);
return React.createElement('View', this.props, children);
}
};
Component.displayName = 'View';
return { [componentName]: Component };
}
This is for testing if the mock component is passed to with specific props through mockFunction
. Then I tried to import the utility and run like below.
import { render } from '@testing-library/react-native';
import { ParentComponent } from 'path/to/ParentComponent';
import { mockComponent } from 'path/to/mockComponent';
const mockChildFunction = jest.fn();
jest.mock('path/to/ChildComponent', () => {
return mockComponent('ChildComponent', mockChildFunction);
});
describe('ParentComponent', () => {
test('checks if mockChildFunction is called', () => {
const specificProps = { ... }
render(<ParentComponent />);
expect(mockChildFunction).toHaveBeenCalled({...specificProps});
});
});
But it didn’t work well. Then I started to investigate why.
What I did
I found the description in the official documents of Jest.
Since calls to
jest.mock()
are hoisted to the top of the file, Jest prevents access to out-of-scope variables. By default, you cannot first define a variable and then use it in the factory. Jest will disable this check for variables that start with the wordmock
. However, it is still up to you to guarantee that they will be initialized on time. Be aware of Temporal Dead Zone.
Basically, The paragraph explains all my mistakes. There were three things which I didn’t know.
- Since calls to
jest.mock()
are hoisted to the top of the file, Jest prevents access to out-of-scope variables. - Jest will disable this check for variables that start with the word
mock
- it is still up to you to guarantee that they will be initialized on time.
Firstly, you can’t access variables defined in your test file from the jest.mock
block. I didn’t know that and tried to call mockComponent
in the block. Then the result returns mockComponent is undefined
. So I put the import in the block.
Secondly, only variables which start with mock
are accessible from the jest.mock
block. I named my mock functions with mock
prefix unintentionally because there are a lot of documents and articles using the prefix. I didn’t know there was the reason behind the naming.
Lastly, you have to care about the timing when mock functions starting with mock
are called by yourself. Technically, when Jest processes jest.mock
blocks, the mock function is undefined yet. So if you assign or execute the mock function, it won’t work as you expect because it’s undefined. To avoid that, you need to use an anonymous function because the mock function isn’t undefined when the anonymous function is called.
The following code snippet is my final output.
import { render } from '@testing-library/react-native';
import { ParentComponent } from 'path/to/ParentComponent';
const mockChildFunction = jest.fn();
jest.mock('path/to/ChildComponent', () => {
const { mockComponent } = require('../../../test/utilities');
const callback = prop => mockChildFunction(props);
return mockComponent('ChildComponent', callback);
});
describe('ParentComponent', () => {
test('checks if mockChildFunction is called', () => {
const specificProps = { ... }
render(<ParentComponent />);
expect(mockChildFunction).toHaveBeenCalled({...specificProps});
});
});
That’s it!