'Testing setState anonymous function call
Consider the following button in JSX:
interface Props {
index: number;
setIndex: Dispatch<SetStateAction<number>>;
}
export function Button({ index, setIndex }: Props) {
return (
<button
type="button"
data-testid="prev_btn"
onClick={() => setIndex(prevIndex => prevIndex - 1)}
>
Previous
</button>
);
}
I would like to write a test in Jest / React Testing Library that renders the button and passes setIndex in as a prop, and then checks that setIndex is called with the correct value. For example:
const mockSetIndex = jest.fn();
const mockProps = {
setIndex: mockSetIndex,
index: 1,
};
describe('Button test', () => {
it('should update the index when the "previous" button is clicked', () => {
const { getByTestId } = render(<Button {...mockProps} />);
const prevBtn = getByTestId('prev_btn');
userEvent.click(prevBtn);
expect(mockSetIndex).toBeCalledWith(0);
});
});
The result I get is a failure with:
expect(jest.fn()).toBeCalledWith(...expected)
Expected: 0
Received: [Function anonymous]
There is another instance of a similar button that increments the index, so I would like to test that the correct behaviour is being made with both buttons.
Solution 1:[1]
You're calling setIndex with a function and not a number. While this is an acceptable argument for setIndex, it's not what you're testing. Either change your function to something like
interface Props {
index: number;
setIndex: Dispatch<SetStateAction<number>>;
}
export function Button({ index, setIndex }: Props) {
return (
<button
type="button"
data-testid="prev_btn"
onClick={() => setIndex(index - 1)}
>
Previous
</button>
);
}
or test that it's passing a function that has the expected behavior
describe('Button test', () => {
it('should update the index when the "previous" button is clicked', () => {
const { getByTestId } = render(<Button {...mockProps} />);
const prevBtn = getByTestId('prev_btn');
userEvent.click(prevBtn);
const passedFunction = mockProps.setIndex.mock.calls[0][0]
expect(passedFunction(1)).toEqual(0);
});
});
Solution 2:[2]
Based on the properties of Button, I'd guess its usage looks something like this:
const Parent = () => {
const [index, setIndex] = useState(1);
return (
<>
<Button index={index} setIndex={setIndex} />
<p data-testid="current_index">Current index: {index}</p>
</>
);
};
This gives the Parent no control over its own state, the Button could call e.g. setIndex(100) whether or not that has any practical meaning. Passing a setter directly to another component is generally a code smell for this reason.
A less tightly coupled structure, where Parent is responsible for its own state, would look like this:
const Parent = () => {
const [index, setIndex] = useState(1);
const decrement = () => setIndex((previousIndex) => previousIndex - 1);
return (
<>
<p>Current index: {index}</p>
<Button onClick={decrement} />
</>
);
};
This is much easier to test:
describe("Button", () => {
it("should signal when the Previous button is clicked", () => {
const onClick = jest.fn();
render(<Button onClick={onClick} />);
userEvent.click(screen.getByRole("button", { name: /previous/i }));
expect(onClick).toHaveBeenCalled();
});
});
(Note I'm following the priority guide to use user-facing, accessible selectors as much as possible.)
Alternatively, if Button is only used within the Parent (as the high level of coupling on state suggests), test it in that context:
describe("Parent", () => {
it("should allow the previous thing to be selected", () => {
render(<Parent />);
userEvent.click(screen.getByRole("button", { name: /previous/i }));
// assert on actual outcome of index changing, e.g.
expect(screen.getByTestId("current_index")).toHaveTextContent("Current index: 0");
});
});
In this case, testing at the boundary between Button and its Parent isn't really appropriate. To indicate this coupling you could move the Button into the module where Parent is defined, and not export it for separate access - treat the fact that the button has been extracted to a separate component as an implementation detail.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | possum |
| Solution 2 | jonrsharpe |
