如何在测试中仅Mock自定义useQuery钩子的data,忽略isLoading等字段?
环境
- Next.js v14 Pages Router
- jest v29.7
- @tanstack/react-query v5.36.0
问题场景
我封装了基于@tanstack/react-query的useUserInfo自定义钩子用于获取用户信息,现在需要测试依赖它的useAccountMenu钩子——这个钩子会根据useUserInfo返回的data生成不同的菜单项。
但测试时直接mockuseUserInfo返回{ data: mockData }会触发类型错误,提示缺少isError、isPending等React Query返回的完整字段。不想每次手动硬编码这些冗余字段,求更优雅的测试方案。
方案1:用React Query官方测试工具生成Mock结果
React Query官方提供了@tanstack/react-query/testing包,其中的mockSuccessQueryResult可以直接生成符合类型要求的成功状态结果,无需手动补全所有字段。
先安装依赖(如果未安装):
npm install --save-dev @tanstack/react-query/testing
修改后的测试代码:
// useAccountMenu.test.ts import { renderHook, waitFor } from '@testing-library/react'; import { mockSuccessQueryResult } from '@tanstack/react-query/testing'; import { useAccountMenu } from './useAccountMenu'; import { useUserInfo } from './useUserInfo'; jest.mock('./useUserInfo'); describe.concurrent('useAccountMenu', () => { it('返回完整菜单项当用户id ≤ 100时', async () => { const mockData = { id: 3, name: 'John', age: 25 }; // 用官方工具生成符合类型的成功结果 jest.mocked(useUserInfo).mockReturnValueOnce(mockSuccessQueryResult(mockData)); const { result } = renderHook(() => useAccountMenu(), { wrapper: YourQueryProvider }); await waitFor(() => { expect(result.current.menuItems).toEqual([ { menuLabel: 'User Setting', to: 'setting/user' }, { menuLabel: 'Language', to: 'setting/language' }, { menuLabel: 'Organization', to: 'setting/organization' }, ]); }); }); it('返回精简菜单项当用户id > 100时', async () => { const mockData = { id: 101, name: 'Jane', age: 30 }; jest.mocked(useUserInfo).mockReturnValueOnce(mockSuccessQueryResult(mockData)); const { result } = renderHook(() => useAccountMenu(), { wrapper: YourQueryProvider }); await waitFor(() => { expect(result.current.menuItems).toEqual([ { menuLabel: 'User Setting', to: 'setting/user' }, { menuLabel: 'Language', to: 'setting/language' }, ]); }); }); });
方案2:自定义Mock辅助函数
如果不想额外安装依赖,可以自己写一个辅助函数,预设常用状态字段,按需生成Mock结果:
// test-utils.ts import type { QueryObserverSuccessResult } from '@tanstack/react-query'; import type { ActiveUserInfo } from './type'; export const createMockUserInfoSuccess = (data: ActiveUserInfo): QueryObserverSuccessResult<ActiveUserInfo, Error> => ({ data, error: null, isError: false, isPending: false, isLoading: false, isSuccess: true, isFetching: false, isRefetching: false, status: 'success', failureCount: 0, refetch: jest.fn(), // 剩余必填字段可按类型要求用undefined或空函数填充 });
测试时直接调用该函数:
// useAccountMenu.test.ts import { createMockUserInfoSuccess } from './test-utils'; // ... jest.mocked(useUserInfo).mockReturnValueOnce(createMockUserInfoSuccess(mockData)); // ...
方案3:重构钩子,隔离依赖简化测试
把useAccountMenu的核心逻辑提取为纯函数,让它直接接收用户信息作为参数,这样测试时无需mock钩子,直接传入测试数据即可:
// useAccountMenu.ts // 提取核心逻辑为纯函数 export const getAccountMenuItems = (userInfo: ActiveUserInfo) => { if (userInfo.id > 100) { return [ { menuLabel: 'User Setting', to: 'setting/user' }, { menuLabel: 'Language', to: 'setting/language' }, ]; } return [ { menuLabel: 'User Setting', to: 'setting/user' }, { menuLabel: 'Language', to: 'setting/language' }, { menuLabel: 'Organization', to: 'setting/organization' }, ]; }; // 原钩子保持原有调用逻辑 export const useAccountMenu = () => { const { data } = useUserInfo(); return { menuItems: getAccountMenuItems(data!) }; // 实际项目中需处理data为null/undefined的情况 };
测试纯函数的代码更简洁:
// useAccountMenu.test.ts import { getAccountMenuItems } from './useAccountMenu'; describe('getAccountMenuItems', () => { it('用户id ≤ 100时返回完整菜单', () => { const mockData = { id: 3, name: 'John', age: 25 }; expect(getAccountMenuItems(mockData)).toEqual([ { menuLabel: 'User Setting', to: 'setting/user' }, { menuLabel: 'Language', to: 'setting/language' }, { menuLabel: 'Organization', to: 'setting/organization' }, ]); }); it('用户id > 100时返回精简菜单', () => { const mockData = { id: 101, name: 'Jane', age: 30 }; expect(getAccountMenuItems(mockData)).toEqual([ { menuLabel: 'User Setting', to: 'setting/user' }, { menuLabel: 'Language', to: 'setting/language' }, ]); }); }); // 如需测试钩子的集成逻辑,再补充钩子测试(此时mock useUserInfo仅需保证data正确,其他字段用辅助函数生成即可)
内容的提问来源于stack exchange,提问作者pop
相关产品推荐
相关产品推荐

