Monkey Patching (Mocking) hooks and methods in React
What is Monkey patching?
A monkey patch is a way for a program to extend or modify supporting system software locally. Monkey patching replace methods / classes / attributes / functions at runtime. It can modify/extend behaviour of a third-party product without maintaining a private copy of the source code and apply the result of a patch at runtime to the state in memory, instead of the source code on disk.
The goal for mocking is to replace something we don’t control with something we do, so it’s important that what we replace it with has all the features we need.
The simplest way to create a Mock Function instance is with jest.fn().
How to mock a method?
Mocking useMutation and useLazyQuery
jest.mock('@apollo/client', () => ({ ...jest.requireActual('@apollo/client'), useMutation: jest.fn(() => [ jest.fn(() => ({ metadata: { message: 'success', code: '200', }, })), { loading: false }, ]), useLazyQuery: jest.fn((arg, { onCompleted }) => [ jest.fn(({ variables }) => { const { input: { userId = null } = {} } = variables; if (userId) { onCompleted({ getAllPostId: { data: { post: [{ originalId: '6044dfe02c68c40012fc6aa2' }], }, }, }); } onCompleted({ getAllPostId: { data: { post: [], }, }, }); return { data: { workflows: [], }, }; }), { loading: false }, ]),}),
In the example above, we have mocked @apollo/client using jest.mock method, jest.requireActual here returns the actual ‘@apollo/client’ module. After that, we have mocked useMutation and useLazyQuery.
The useMutation
result is a tuple with a mutate function in the first position and an object representing the mutation result in the second position. That’s why we have returned a mock function and an object in an array.
Similarly, The useLazyQuery result is a tuple with a query function in the first position and an object representing the mutation result in the second position. In the first argument, we have a query function in which we have called onCompleted method to increase the code coverage. The response received from useLazyQuery is mocked and then passed as an argument to onCompleted method.
Once a method /hook is mocked using jest.mock it can be again mocked using mockImplementation method.
it('Should mock useMutation again', async () => { useMutation.mockImplementation(jest.fn(() => { return ([ jest.fn(() => ({ throw new Error('test failure scenario'); }), )]); })); await new Promise((resolve) => setTimeout(resolve, 0)); expect(screen.getByTestId('hit-mutation')).toBeTruthy();});
Mocking React Component
Let us consider a react component Wizard having children (Component1 / Component2) inside children (Wizard.Step)
<Wizard title="title">
<Wizard.Step onClick={handleStep} />
<Component1 />
</Wizard.Step>
<Wizard.Step>
<Component2 />
<Component3 />
</Wizard.Step>
</Wizard>
Many times these type of component does not accept any data-testid and create a problem in accessing some components which are present inside them. So these kinds of problems can be resolved used monkey patching.
jest.mock('@ui/components', () => ({ ...jest.requireActual('@ui/components'), Wizard: jest.fn((props) => { const { title, children } = props; return ( <> <h1>{title}</h1>
// return Component1, Component2 and Component3
{
children
.map(({ props: { children: child } }) => child)
.flat()
} </> ); }),}));
In this example, the Wizard component is mocked and from its ‘props’, children are destructured. Then we have used map() on ‘children’ to iterate over each Wizard’s ‘step’ and get an array of the components inside it. Now, we finally get an array with subarrays for each step’s components which somewhat looks like [Component1,[Component2, Component3]]. Then, flat() is called to flatten this array and return a single array of all the components extracted i.e. Component1, Component2, and Component3.