如何测试React Hook返回的单个函数在组件中是否被调用?
如何仅Mock自定义Hook返回的单个函数并保留其他逻辑?
问题背景
你想测试组件调用自定义Hook返回的setSomeValue函数是否执行,但当前的Mock方式会丢失Hook内其他逻辑,导致其他测试失败。示例代码如下:
自定义Hook useSomeHook
import { useState } from 'react'; export const useSomeHook = () => { const [someValue, setSomeValue] = useState(); const [someOtherValue, setSomeOtherValue] = useState(); // 其他逻辑... return { someValue, setSomeValue, someOtherValue, setSomeOtherValue, } }
使用Hook的组件 SomeComponent
export const SomeComponent = () => { const { setSomeValue } = useSomeHook(); return (<button onClick={() => setSomeValue('hey')}>hello</button>); }
失败的Mock尝试
const mockSetSomeValue = jest.fn(); jest.mock('./useSomeHook.js', () => { const actual = jest.requireActual('./useSomeHook.js'); return { ...actual, // 此方式丢失了Hook内其他内容,导致其他测试失败 useSomeHook: () => ({ setSomeValue: mockSetSomeValue, }) } }); describe('SomeComponent', () => { it('should call setSomeValue when button is clicked', () => { render(<SomeComponent />); // 触发按钮点击事件 expect(mockSetSomeValue).toHaveBeenCalledWith('hey'); }); });
可行解决方案
方案1:调用实际Hook并覆盖单个返回值
在mock函数里调用真实的Hook,然后仅替换需要Mock的函数,这样能保留Hook内的所有原有逻辑和其他返回值:
const mockSetSomeValue = jest.fn(); jest.mock('./useSomeHook.js', () => { const actual = jest.requireActual('./useSomeHook.js'); return { ...actual, useSomeHook: () => { // 调用真实的Hook获取完整返回对象 const originalHookReturn = actual.useSomeHook(); // 仅替换需要Mock的函数 return { ...originalHookReturn, setSomeValue: mockSetSomeValue, }; }, }; }); describe('SomeComponent', () => { it('should call setSomeValue when button is clicked', () => { render(<SomeComponent />); fireEvent.click(screen.getByText('hello')); expect(mockSetSomeValue).toHaveBeenCalledWith('hey'); }); });
方案2:使用jest.spyOn动态Mock(更灵活)
如果只需要在单个测试用例中Mock,而不是全局Mock,可以用jest.spyOn来临时替换目标函数,测试完成后恢复:
import * as useSomeHookModule from './useSomeHook'; describe('SomeComponent', () => { it('should call setSomeValue when button is clicked', () => { // 保存原始Hook const originalUseSomeHook = useSomeHookModule.useSomeHook; const mockSetSomeValue = jest.fn(); // 用spyOn替换Hook,调用原始逻辑后覆盖setSomeValue jest.spyOn(useSomeHookModule, 'useSomeHook').mockImplementation(() => { const originalReturn = originalUseSomeHook(); return { ...originalReturn, setSomeValue: mockSetSomeValue, }; }); render(<SomeComponent />); fireEvent.click(screen.getByText('hello')); expect(mockSetSomeValue).toHaveBeenCalledWith('hey'); // 恢复原始Hook,避免影响其他测试 useSomeHookModule.useSomeHook.mockRestore(); }); });
方案3:MockuseState跟踪调用(针对状态更新函数)
因为setSomeValue是useState返回的更新函数,也可以直接MockuseState来跟踪它的调用,这种方式不需要修改Hook的Mock:
import { useState } from 'react'; jest.mock('react', () => ({ ...jest.requireActual('react'), useState: jest.fn(), })); describe('SomeComponent', () => { it('should call setSomeValue when button is clicked', () => { const mockSetSomeValue = jest.fn(); // 让useState返回原始状态和Mock的更新函数 useState.mockImplementation((initialState) => { // 判断是否是对应someValue的useState调用,或者按顺序处理 if (initialState === undefined) { // 匹配第一个useState的初始值 return [undefined, mockSetSomeValue]; } // 其他useState调用返回原始逻辑 const originalUseState = jest.requireActual('react').useState; return originalUseState(initialState); }); render(<SomeComponent />); fireEvent.click(screen.getByText('hello')); expect(mockSetSomeValue).toHaveBeenCalledWith('hey'); }); });
注意:方案3需要注意
useState的调用顺序,如果Hook内的useState顺序变化,测试会失效,适合简单场景。
内容的提问来源于stack exchange,提问作者Charlie Stanard
相关产品推荐
相关产品推荐

