【问题标题】:Unit testing React components that access a global store单元测试访问全局存储的 React 组件
【发布时间】:2025-12-17 07:55:02
【问题描述】:

首先,如果这里不适合提出这个问题或将其移至正确的地方,请告诉我。

我开始为一些 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);

我的问题是,当我尝试在测试文件中renderPlayerSelector 时,我遇到了一个错误,因为测试无权访问该存储:

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 做同样的事情。我不知道我是否使事情过于复杂,可能应该找到另一种测试方法,或者我是否错过了一个明显的答案。无论如何,如果您能分享任何提示或指导,我将不胜感激。

【问题讨论】:

    标签: reactjs unit-testing jestjs react-testing-library


    【解决方案1】:

    当您测试使用来自提供者的上下文的组件时,您还需要渲染提供者以提供上下文。 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');
    });
    

    如果您发现自己需要大量编写 wrapper 实用程序,您也可以创建一个 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');
    });
    

    【讨论】:

    • 你是救生员!我实施了您的建议,然后通过导入包含商店内容的 .js 文件来模拟 initialState。最后,我必须将 PlayerSelection 组件包装在 MemoryRouter 中。我想让您知道,我非常感谢您花时间回答我的问题并为我指明正确的方向。非常感谢!