Search code examples
reactjsunit-testingtestingjestjsreact-testing-library

Jest test returning [function anonymous] when testing setState hook


I asked this question previously, but this time I've created a minimal reproducible example.

Below is my App, which contains an object state, and renders a Sidebar component:

import Sidebar from './Sidebar';
import { useState } from 'react';

function App() {
  const [state, setState] = useState({
    status: 'ok',
    count: 0,
  });
  return <Sidebar state={state} setState={setState} />;
}

export default App;

Here is my Sidebar component, which takes the count property from the state object, and updates the count property when the user clicks the button:

export default function Sidebar({ state, setState }) {
  function handleButtonClick() {
    setState((prevState) => ({
      ...prevState,
      count: prevState.count + 1,
    }));
  }
  return (
    <>
      <div>count: {state.count} </div>
      <button data-testid='button' onClick={handleButtonClick}>
        Increase Count
      </button>
    </>
  );
}

Below is the Jest test for my Sidebar. I've mocked the state and setState, and am expecting setState to be called with the object after the user clicks the 'Increase Count' button:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Sidebar from './Sidebar';

let state = () => ({
  status: 'ok',
  count: 0,
});
const setState = jest.fn();

test('sets count on button click', () => {
  render(<Sidebar state={state} setState={setState} />);

  const button = screen.getByTestId('button');
  userEvent.click(button);
  expect(setState).toHaveBeenCalledWith({ status: 'ok', count: 1 });
});

However, below is the result of me running the Sidebar test. It receives [Function anonymous]. enter image description here

I'm not sure if this is because my expect is written incorrectly, or because I didn't mock state/setState properly. Any help would be greatly appreciated.


Solution

  • In general, don't mock setState or the React API. These are implementation details of your components, so testing them is against the RTL philosophy.

    It's an antipattern to pass state setters and getters directly into child components because it couples the parent and child's implementations too tightly, stifling refactoring. The child knows too much about the parent (that it uses state hooks, as well as status which it never uses) and is responsible for too much (modifying the parent's state correctly).

    Specifically, move handleButtonClick to the parent and pass it down as a prop. This hides the setState call, decoupling the components from the hooks API and making it impossible for the child to corrupt the parent's state with a malformed setState call. It also gives the parent power over when to re-render.

    In your simplified example, there's no reason for App to control this state since it's never rendered anywhere except for the one child that also sets it. I assume App needs access to the data elsewhere, preventing you from being able to lower the state into the child and eliminate the prop.

    One litmus test for coupling is asking "if I were to refactor class components to functional components or vice-versa, would I have to rewrite all of my tests?" If the answer is yes, you're testing implementation details.

    After making your handler generic, you can still mock it and assert that it was called. However, the better (though not mutually-exclusive) test is to render the parent and the child and simply assert that the user-visible effect occurred after the button click. This tests the handler and gives full coverage without mocking, with the "drawback" that your unit of code is now 2 components working in tandem. This is acceptable since that's the distance the state spans anyway, so the test might as well acknowledge that as the unit under test.