When we have
function ParentComponent() {
const [state, setState] = useState({
name: 'Rob',
age: '55'
});
return <ChildComponent state={state} setState={setState} />;
}
and inside ChildComponent
something like this happens:
setState({
name: 'Alice',
age: 44
});
we can do
const mockSetter = jest.fn();
render(<ChildComponent state={{ name: 'Rob', age: 55 }} setState={mockSetter} />);
// interact with ChildComponent to make it call `setState`
expect(mockSetter).toHaveBeenCalledWith({
name: 'Alice',
age: 55
});
But how do I test that the new state has been set to { name: 'Alice', age: 55 }
in case the setState
prop inside ChildComponent
is called with a function instead of an object? For example:
setState(prevState => ({ ...prevState, name: 'Alice' })
In this case mockSetter
will be called with a function so my original toHaveBeenCalledWith
check is no longer valid. It expects an object { name: 'Alice', age: 55 }
but gets a function prevState => ({ ...prevState, name: 'Alice' }
.
How do I check that the state
has been set to { name: 'Alice', age: 55 }
after a ChildComponent
interaction (even though I don't have the state
variable to refer to in my tests)?
CodePudding user response:
First of all, mock setter function of the useState
hook will break the real implementation of it. Your test may pass based on the mock setter function, but the actual code may not run correctly.
But if you insist to do so, you can use jest.fn().mockImplementation()
to create mock implementation for the setter function of useState
. (Not recommended!! React doesn't know how to update the state anymore with this mock setter function!)
index.tsx
:
import React, { useState } from 'react';
export const ChildComponent = ({ state, setState }) => {
return (
<div
onClick={() => {
setState((prevState) => ({ ...prevState, name: 'Alice' }));
}}
>
ChildComponent
</div>
);
};
index.test.tsx
:
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { ChildComponent } from './';
describe('73406702', () => {
test('should pass', () => {
const prevState = { name: 'Rob', age: 55 };
let nextState;
const mockSetter = jest.fn().mockImplementation((callback) => {
nextState = callback(prevState);
});
render(<ChildComponent state={prevState} setState={mockSetter} />);
fireEvent.click(screen.getByText(/ChildComponent/));
expect(nextState).toEqual({ name: 'Alice', age: 55 });
});
});
We can provide prevState
by ourselves, and get the nextState
.
This mock implementation is only used for testing the state merging logic { ...prevState, name: 'Alice' }
. React doesn't know how to update the state and re-render the component anymore with this mock setter function. Because we didn't provide this feature for the mock setter function. The real implementation of the setter function is complicated.
Test result:
PASS stackoverflow/73406702/index.test.tsx (10.451 s)
73406702
✓ should pass (28 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 10.948 s, estimated 12 s
In the end, the testing philosophy for react component, or, any UI component is to test the component behavior stand from the user's perspective. You can think it's black-box testing.