newdev
newdev

Reputation: 189

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.

Upvotes: 1

Views: 1281

Answers (1)

Drew Reese
Drew Reese

Reputation: 202801

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');
});

Upvotes: 1

Related Questions