You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React组件测试:useEffect结合useState的逻辑测试与类型错误修复求助

React组件测试:useEffect结合useState的逻辑测试与类型错误修复求助

嘿,我来帮你一步步搞定这些问题!咱们先从最头疼的类型错误入手,再逐步拆解useEffect和mobileMenu状态的测试逻辑。

一、先修复useState Mock的类型错误

你遇到的类型不匹配问题,核心原因是React的useState返回的是[状态值, Dispatch<状态值>]类型,但你之前的mock函数只返回了[boolean, jest.Mock],和React要求的类型对不上。而且你的组件里有两个不同类型的useState(一个存boolean的mobileMenu,一个存NaviLinksProps|null的linkProps),所以不能用单一的mock逻辑应付。

给你调整后的mock代码,完美适配类型:

import { Dispatch } from 'react';
// 别忘了导入你的NaviLinksProps类型
import { NaviLinksProps } from '@mcdayen/prop-types';

describe('App', () => {
  let setMobileMenu: jest.Mock;
  let setLinkProps: jest.Mock;

  beforeEach(() => {
    // 每次测试前重置所有mock,避免用例污染
    jest.clearAllMocks();
    
    // 给两个setter分别创建mock函数
    setMobileMenu = jest.fn();
    setLinkProps = jest.fn();

    // 模拟useState,根据初始值类型区分两个useState调用
    jest.spyOn(React, 'useState').mockImplementation((initialValue: any) => {
      if (typeof initialValue === 'boolean') {
        // 对应组件里的const [mobileMenu, setMobileMenu] = useState(false);
        return [false, setMobileMenu] as [boolean, Dispatch<boolean>];
      } else {
        // 对应组件里的const [linkProps, setLinkProps] = useState<NaviLinksProps | null>(null);
        return [null, setLinkProps] as [NaviLinksProps | null, Dispatch<NaviLinksProps | null>];
      }
    });
  });

  // 下面写你的测试用例...
});

这样修改后,TypeScript就不会再报类型不匹配的错误了,因为我们明确指定了返回值的类型和React的useState一致。

二、测试useEffect的逻辑和mobileMenu状态变化

你的组件里有两个useEffect,咱们分开测试:

1. 测试挂载时的第一个useEffect(空依赖数组)

这个useEffect做了两件事:

  • 初始化linkProps为initialNaviLinksProps
  • 检查id为mobileMenu的元素是否存在offsetParent,如果存在就调用mobileMenuHandler(也就是切换mobileMenu状态)

我们可以通过mockdocument.getElementById来模拟两种场景:

it('初始化时设置linkProps,且当mobileMenu元素可见时切换状态', () => {
  // 模拟mobileMenu元素存在且可见(offsetParent不为null)
  const mockMobileEl = { offsetParent: document.createElement('div') } as HTMLElement;
  jest.spyOn(document, 'getElementById').mockReturnValue(mockMobileEl);

  // 渲染组件
  render(
    <BrowserRouter>
      <Provider store={createTestStore()}>{<App />}</Provider>
    </BrowserRouter>
  );

  // 验证linkProps被初始化为initialNaviLinksProps
  expect(setLinkProps).toHaveBeenCalledWith(initialNaviLinksProps);
  // 验证mobileMenuHandler被调用(因为handler用了函数式更新,所以这里要检查是否传入了函数)
  expect(setMobileMenu).toHaveBeenCalledWith(expect.any(Function));
});

it('初始化时设置linkProps,但mobileMenu元素不可见时不切换状态', () => {
  // 模拟mobileMenu元素存在但不可见(offsetParent为null)
  const mockMobileEl = { offsetParent: null } as HTMLElement;
  jest.spyOn(document, 'getElementById').mockReturnValue(mockMobileEl);

  render(
    <BrowserRouter>
      <Provider store={createTestStore()}>{<App />}</Provider>
    </BrowserRouter>
  );

  expect(setLinkProps).toHaveBeenCalledWith(initialNaviLinksProps);
  // 验证setMobileMenu没有被调用
  expect(setMobileMenu).not.toHaveBeenCalled();
});

2. 测试依赖mobileMenu的第二个useEffect

这个useEffect会根据mobileMenu的状态动态更新linkProps:

  • 当mobileMenu为true时,返回完整的initialNaviLinksProps
  • 当mobileMenu为false时,给classProps加上hidden后缀

我们可以通过修改mock的useState返回值,再重新渲染组件来测试不同状态下的逻辑:

it('mobileMenu状态变化时正确更新linkProps', () => {
  // 第一步:模拟mobileMenu初始为false
  jest.spyOn(React, 'useState').mockImplementation((initialValue: any) => {
    if (typeof initialValue === 'boolean') {
      return [false, setMobileMenu] as [boolean, Dispatch<boolean>];
    } else {
      return [null, setLinkProps] as [NaviLinksProps | null, Dispatch<NaviLinksProps | null>];
    }
  });

  const { rerender } = render(
    <BrowserRouter>
      <Provider store={createTestStore()}>{<App />}</Provider>
    </BrowserRouter>
  );

  // 验证mobileMenu为false时的linkProps更新逻辑
  // 这里setLinkProps的第二次调用是useEffect里的函数式更新,取第二个调用的参数
  const updateFn = setLinkProps.mock.calls[1][0];
  // 模拟当前classProps为空的情况,执行更新函数
  const resultWhenFalse = updateFn({ classProps: '' });
  expect(resultWhenFalse).toEqual({ ...initialNaviLinksProps, classProps: ' hidden' });

  // 第二步:模拟mobileMenu变为true,重新渲染组件
  jest.spyOn(React, 'useState').mockImplementation((initialValue: any) => {
    if (typeof initialValue === 'boolean') {
      return [true, setMobileMenu] as [boolean, Dispatch<boolean>];
    } else {
      return [null, setLinkProps] as [NaviLinksProps | null, Dispatch<NaviLinksProps | null>];
    }
  });

  rerender(
    <BrowserRouter>
      <Provider store={createTestStore()}>{<App />}</Provider>
    </BrowserRouter>
  );

  // 验证mobileMenu为true时的linkProps更新逻辑
  const newUpdateFn = setLinkProps.mock.calls[2][0];
  const resultWhenTrue = newUpdateFn({ classProps: 'test' });
  expect(resultWhenTrue).toEqual({ ...initialNaviLinksProps }); // 没有hidden后缀
});

三、额外的测试小技巧

  • 不要直接mock整个React模块,用jest.spyOn单独mock需要的钩子,这样不会破坏React其他内置功能。
  • 每次测试后记得清理mock(比如jest.clearAllMocks()),避免测试用例之间互相干扰。
  • 如果你想测试Redux的dispatch逻辑,比如fetchCartDetails是否被调用,可以给store的dispatch加个spy:
it('组件挂载时会触发fetchCartDetails的dispatch', () => {
  const store = createTestStore();
  const dispatchSpy = jest.spyOn(store, 'dispatch');

  render(
    <BrowserRouter>
      <Provider store={store}>{<App />}</Provider>
    </BrowserRouter>
  );

  expect(dispatchSpy).toHaveBeenCalledWith(fetchCartDetails());
});

备注:内容来源于stack exchange,提问作者hasina rajik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.23 15:22:41