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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 01:15:30