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
相关产品推荐
相关产品推荐

