单元测试访问全局存储的 React 组件

Unit testing React components that access a global store

首先,请让我知道这里是否适合提出此问题或将其移至正确的位置。

我开始为一些 React 组件编写单元测试。其中一个没有任何道具,但依赖于 store:

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

其中 userStore() 来自:

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

我的问题是,当我在测试文件中尝试 render PlayerSelector 时,我 运行 出错了,因为测试无法访问该存储:

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

结果是TypeError: object null is not iterable (cannot read property Symbol(Symbol.iterator))指向这一行:

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

经过大量谷歌搜索后,我发现了一些有用的资源,指出了如何模拟上下文——但没有说明如何为 reducer 做同样的事情。我不知道我是否使事情过于复杂并且可能应该找到另一种测试方法,或者我是否遗漏了一个明显的答案。无论如何,如果您能分享任何提示或指导,我将不胜感激。

当您测试使用来自提供程序的上下文的组件时,您还需要呈现提供程序以提供上下文。 render 采用第二个选项参数,可以包含 wrapper 来包装被测试的组件。

示例:

const customWrapper = ({ children }) => <SomeProvider>{children}</SomeProvider>;

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

使用类似 redux 的提供程序,您可以为我们创建一个 StoreProvider 实例来创建用于测试的包装器。

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

如果您发现自己需要大量编写包装器实用程序,您还可以创建一个 custom render function,它或多或少与上述内容重复。

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

测试

import { renderWithState } from '../path/to/utils';

...

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