如何使用Jest测试自定义Hook的入参回调逻辑
React Native 组件单元测试Mutation回调覆盖率问题解决方案
问题核心原因
当前usePaymentMutation的mock实现直接返回固定值,没有捕获组件传入的成功、失败回调参数,测试过程中无法触发这两段回调逻辑,才会出现覆盖率缺失。不需要抽离回调到独立文件,只要调整mock写法捕获回调入参,在测试用例中手动模拟回调触发即可覆盖全部逻辑。
具体实现步骤
- 第一步:调整mock逻辑,捕获组件传入的回调函数
把mock中用到的函数、捕获回调的变量提到测试作用域顶部,同时在每个用例执行前清空状态,避免用例间污染:
// 存储捕获到的成功、失败回调 let capturedOnSuccess: ((data: any) => void) | undefined; let capturedOnError: ((error: {message: string}) => void) | undefined; // mock方法统一提至顶层,方便断言 const mockGetPaymentDetails = jest.fn(); const mockNavigateToScreen = jest.fn(); jest.mock('../bootstrap', () => ({ useFlowNavigation: jest.fn().mockReturnValue({ navigateToScreen: mockNavigateToScreen, }), })); jest.mock('../queries', () => ({ // mock实现中接收usePaymentMutation的入参,保存成功、失败回调 usePaymentMutation: jest.fn().mockImplementation((_config, onSuccess, onError) => { capturedOnSuccess = onSuccess; capturedOnError = onError; return { isLoading: false, mutate: mockGetPaymentDetails }; }), })); // 每个用例执行前重置所有mock和捕获的回调 beforeEach(() => { jest.clearAllMocks(); capturedOnSuccess = undefined; capturedOnError = undefined; });
- 第二步:新增成功场景测试用例,覆盖成功回调逻辑
it('navigates to payment screen when payment details fetch succeeds', () => { const { getByLabelText, getByText } = render(<SelectPaymentIdScreen />); // 模拟用户输入订单号、点击结账按钮 fireEvent.changeText(getByLabelText('TextInputField'), '1234'); fireEvent.press(getByText('CHECKOUT')); // 手动触发捕获的成功回调,模拟接口返回成功 expect(capturedOnSuccess).toBeDefined(); capturedOnSuccess?.({ /* 填入符合成功条件的mock返回数据 */ }); // 断言成功后的跳转逻辑被触发 expect(mockNavigateToScreen).toHaveBeenCalledWith('PAYMENT_SCREEN'); });
- 第三步:新增异常场景测试用例,覆盖错误分支
这里要注意你贴出的组件代码存在语法问题:成功回调里return语句后的}直接闭合了回调函数,setPaymentError('Payment provider not supported!')被写到了回调函数外部,这段代码永远不会执行。请先修正回调的判断逻辑(比如增加返回数据校验,不满足支持条件时才展示错误),再编写对应测试:
it('shows unsupported psp error when success response data is invalid', () => { const { getByLabelText, getByText, findByText } = render(<SelectPaymentIdScreen />); fireEvent.changeText(getByLabelText('TextInputField'), '1234'); fireEvent.press(getByText('CHECKOUT')); // 触发成功回调,传入不满足支持条件的返回数据 capturedOnSuccess?.({ /* 构造psp不匹配的mock数据 */ }); // 断言错误提示展示 expect(findByText('Payment provider not supported!')).toBeTruthy(); }); it('shows error message when payment request fails', () => { const { getByLabelText, getByText, findByText } = render(<SelectPaymentIdScreen />); fireEvent.changeText(getByLabelText('TextInputField'), '1234'); fireEvent.press(getByText('CHECKOUT')); // 手动触发失败回调,模拟接口报错 const testError = { message: 'Request timeout' }; expect(capturedOnError).toBeDefined(); capturedOnError?.(testError); // 断言接口错误信息被展示 expect(findByText('Request timeout')).toBeTruthy(); });
优化提示
- 不需要改动业务代码抽离回调,这种通过mock捕获闭包内回调、手动触发的方式,是React组件单元测试中覆盖hook回调逻辑的标准方案,测试逻辑和真实用户操作路径完全一致。
- 如果
usePaymentMutation是基于react-query封装的hook,后续也可以不用手动mock,使用@testing-library/react-native配合QueryClientProvider在测试环境构造真实的hook调用,直接用waitFor等待异步逻辑执行完成即可,不过手动mock捕获回调的方式执行速度更快,用例更稳定。
内容的提问来源于stack exchange,提问作者MHB2011
相关产品推荐
相关产品推荐

