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

React Testing Library:如何测试子组件触发的父组件方法?

问题:React Testing Library测试跨层级触发的Recaptcha回调方法

我正在将一个基于React 16、使用Enzyme测试的应用升级到React 17,并用React Testing Library重写测试。当前遇到的问题是:测试子/孙组件触发父/祖父组件方法的场景。

组件层级:App => SubmissionForm => Tab1(多标签表单的一个独立标签组件)。用户点击Tab1的提交按钮后,会调用SubmissionForm的一系列方法,最终执行verifyRecaptchaScore,该方法调用react-google-invisible-recaptcha的execute方法,进而触发App组件的onResolved回调。Recaptcha组件和onResolved均定义在App中:

// App.js
...

async onResolved() {
  const token = await this.recaptcha.current.getResponse();
  this.props.apiSubmission.handleInput({
    target: { name: "reCaptchaValue", value: token }
  });
}

...

<Recaptcha
   ref={refCaptcha}
   sitekey={sitekey}
   onResolved={this.onResolved}
/>

旧的Enzyme测试可以直接调用组件实例方法完成测试:

...

const unconnectedSetup = async () => {
  const setupProps = { ...defaultProps };
  return shallow(<AppUnconnected {...setupProps} />);
};

...

describe("<App />", () => {
  ...

  describe("Misc methods", () => {
    it("onResolved calls recaptcha.getResponse and saves recaptcha token to redux store", async () => {
      wrapper = await unconnectedSetup();
      getResponseMock = jest
        .fn()
        .mockImplementation(() => Promise.resolve("token"));
      wrapper.instance().recaptcha = {
        getResponse: getResponseMock
      };
      await wrapper.update();
      await wrapper.instance().onResolved();
      expect(getResponseMock.mock.calls.length).toBe(1);
      await getResponseMock().then(() => {
        expect(handleInputMock).toHaveBeenCalledWith({
          target: { name: "reCaptchaValue", value: "token" }
        });
      });
    });
    ...
  });

改用React Testing Library后,我尝试模拟用户操作链路(加载App→点击Next加载Tab1→点击Submit触发方法链),但点击Submit后无法触发后续方法,测试失败。测试代码如下:

describe("Misc methods", () => {
    it("onResolved calls recaptcha.getResponse and saves recaptcha token to redux store", async () => {
      getResponseMock = jest
        .fn()
        .mockImplementation(() => Promise.resolve("token"));

      const props = {
        recaptcha: {
          current: {
            getResponseMock
          }
        },
        formValues: {
          ...generateSubmissionBody
        }
      };

      // 模拟用户点击'Next'
      const user = userEvent.setup();
      const { getByTestId, getByRole } = await setup({ ...props });
      const nextButton = getByTestId("button-next");
      await userEvent.click(nextButton);

      // 验证点击'Next'后Tab1已渲染
      const tab1Form = getByRole("form");
      await waitFor(() => {
        expect(tab1Form).toBeInTheDocument();
      });

      // 模拟提交默认表单值
      const submitButton = getByTestId("button-submit");

      await waitFor(() => {
        userEvent.click(submitButton);
        // 也尝试过fireEvent.submit(tab1Form);
      });

      // 期望mock被调用一次
      await waitFor(() => {
        expect(getResponseMock).toHaveBeenCalled();
      });

      // 上述测试失败;mock未被调用

      // 恢复mock
      getResponseMock.mockRestore();
    });

另外验证过fireEvent.submit(tab1Form)能触发自定义的handleSubmit mock,但无法触发真实的提交逻辑。


解答

一、测试思路调整:拆分测试场景,降低链路依赖

React Testing Library提倡测试用户可见的行为结果,而非内部方法调用链。针对你的场景,建议拆分两种测试:

  1. 单元测试onResolved方法本身
    不用模拟全链路用户操作,直接测试方法的核心逻辑,和Enzyme思路类似但适配RTL的写法:
    it("onResolved获取recaptcha token并更新redux", async () => {
      const mockGetResponse = jest.fn().mockResolvedValue("test-token");
      const mockHandleInput = jest.fn();
    
      // 创建App实例,注入mock依赖
      const appInstance = new App({
        apiSubmission: { handleInput: mockHandleInput },
        // 传入其他必要props
      });
      appInstance.recaptcha = { current: { getResponse: mockGetResponse } };
    
      await appInstance.onResolved();
    
      expect(mockGetResponse).toHaveBeenCalledTimes(1);
      expect(mockHandleInput).toHaveBeenCalledWith({
        target: { name: "reCaptchaValue", value: "test-token" }
      });
    });
    
  2. 集成测试用户提交流程
    验证用户点击Submit后的最终行为(比如redux状态更新),但需要mock第三方Recaptcha组件,避免依赖外部服务:
    • 核心是模拟Recaptcha组件的execute方法触发onResolved,而非依赖真实组件逻辑

二、当前全链路测试失败的排查方向

  1. 第三方Recaptcha组件未被mock
    真实的Recaptcha组件在测试环境中无法正常加载或执行execute方法,导致onResolved从未触发。用Jest mock掉整个组件:
    // 在测试文件顶部mock组件
    jest.mock('react-google-invisible-recaptcha', () => {
      const MockRecaptcha = ({ ref, onResolved }) => {
        // 模拟ref对象,调用execute时触发onResolved
        if (ref) {
          ref.current = {
            execute: () => onResolved(),
            getResponse: () => Promise.resolve('mock-token')
          };
        }
        return null;
      };
      return MockRecaptcha;
    });
    
  2. 异步操作未正确等待
    userEvent.click是异步操作,需要用await等待完成,不能直接放在waitFor中调用:
    // 修正提交逻辑
    const submitButton = getByTestId("button-submit");
    await user.click(submitButton); // 使用之前定义的user实例
    
    await waitFor(() => {
      expect(getResponseMock).toHaveBeenCalled();
    });
    
  3. 表单提交默认行为未阻止
    如果表单的onSubmit没有调用e.preventDefault(),页面会刷新导致测试状态丢失。检查Tab1或SubmissionForm的提交逻辑:
    const handleSubmit = async (e) => {
      e.preventDefault(); // 必须添加这一行阻止默认刷新
      // 后续业务逻辑
    };
    
  4. 中间方法链路未正确传递
    用Jest spy监听中间方法,验证是否被触发:
    // 监听SubmissionForm的verifyRecaptchaScore方法
    jest.spyOn(SubmissionForm.prototype, 'verifyRecaptchaScore').mockResolvedValue();
    
    // 点击提交后验证方法是否被调用
    await user.click(submitButton);
    expect(SubmissionForm.prototype.verifyRecaptchaScore).toHaveBeenCalled();
    

三、Recaptcha回调测试的通用方案

  • Mock第三方组件:完全隔离外部服务依赖,模拟组件的核心行为(比如触发回调)
  • 测试最终结果:验证用户操作后的实际影响(如redux状态、API调用),而非中间方法调用
  • 拆分测试粒度:单元测试核心逻辑,集成测试用户流程,避免全链路测试的复杂性

内容的提问来源于stack exchange,提问作者rg_

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 11:57:08