Unit testing React components that access a global store

First off, please let me know if this is not the right place to ask this question or move it to the right place.

I am starting to write unit tests for some React components. One of them does not have any props but depends on a store:

const PlayerSelector = () => {
  const classes = useStyles();
  const divRef = useRef(null);
  const [{ playerList }, dispatch] = useStore();
...

where userStore() comes from:

export const StateContext = createContext(null);

export const StoreProvider = ({ initialState, reducer, children }) => (
  <StateContext.Provider
    value={useReducer(reducer, initialState)}
  >
    {children}
  </StateContext.Provider>
);

export const useStore = () => useContext(StateContext);

My problem is that when I try to render PlayerSelector in the test file, I run into an error because the test does not have access to that store:

it('loads component', () => {
  const { queryByRole } = render(<PlayerSelector />);
  expect(queryByRole('button')).toHaveAttribute('aria-label');
});

The result is TypeError: object null is not iterable (cannot read property Symbol(Symbol.iterator)) pointing to this line:

const [{ firmList }, dispatch] = useStore();

After googling a lot, I found some helpful resources indicating how to mock context - but not how to do the same for a reducer. I do not know if I am overcomplicating things and should probably find another way to test, or if I am missing an obvious answer. In any event, I would appreciate any tips or direction you can share.

1 answer

  • answered 2021-01-13 00:16 Drew Reese

    When you are testing a component that consumes a context from a provider you also need to render the provider in order to provide the context. The render takes a second options argument that can include a wrapper to wrap the component being tested.

    Example:

    const customWrapper = ({ children }) => <SomeProvider>{children}</SomeProvider>;
    
    it('loads component', () => {
      const { queryByRole } = render(
        <PlayerSelector />,
        {
          wrapper: customWrapper
        },
      );
      expect(queryByRole('button')).toHaveAttribute('aria-label');
    });
    

    With your redux-like provider you can create us an instance of your StoreProvider to create a wrapper for testing.

    it('loads component', () => {
      const { queryByRole } = render(
        <PlayerSelector />,
        {
          wrapper: ({ children }) => (
            <StoreProvider
              initialState={{}} // <-- any initial state object
              reducer={rootReducerFunction} // <-- any reducer function
            >
              {children}
            </StoreProvider>
          ),
        },
      );
      expect(queryByRole('button')).toHaveAttribute('aria-label');
    });
    

    If you find yourself needing to write the wrapper utility a lot you can also create a custom render function, which more or less duplicates the above.

    const StateProvider = ({ children }) => (
      <StoreProvider
        initialState={{}} // <-- any initial state object
        reducer={rootReducerFunction} // <-- any reducer function
      >
        {children}
      </StoreProvider>
    );
    
    export const renderWithState = (ui, options) => {
      return render(ui, { wrapper: StateProvider, ...options });
    }
    

    test

    import { renderWithState } from '../path/to/utils';
    
    ...
    
    it('loads component', () => {
      const { queryByRole } = renderWithState(<PlayerSelector />);
      expect(queryByRole('button')).toHaveAttribute('aria-label');
    });