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

使用@testing-library/react时await waitFor测试失败问题咨询

问题:useFunnel Hook测试中await waitFor失败但直接调用成功的原因

我正在测试自定义React Hook——useFunnel,这个Hook返回的Funnel组件同一时间只显示一个Funnel.Step子组件,初始显示第一个Step(<div>Step 1 UI</div>)。

我用act()触发步骤切换到step2,为了等待组件重新渲染,用await waitFor包裹断言时测试失败,但直接调用waitFor却能成功。这让我很困惑:既然直接用waitFor断言通过,说明组件已经完成更新(Step1卸载、Step2挂载),为什么加了await反而失败?

另外,我想使用await waitFor是为了在组件更新后打印界面(console.log(screen.debug(undefined, Infinity)))。


测试代码

describe("useFunnel hook", () => {
  it("should successfully render the second step in the funnel", () => {
    const { result } = renderHook(() => useFunnel(["step1", "step2"]));
    const [Funnel, changeStep] = result.current;

    const { getByText, queryByText } = render(
      <React.Fragment>
        <Funnel>
          <Funnel.Step name="step1">Step 1 UI</Funnel.Step>
          <Funnel.Step name="step2">Step 2 UI</Funnel.Step>
        </Funnel>
      </React.Fragment>
    );

    act(() => {
      changeStep("step2");
    });

    waitFor(() => { // `await waitFor` fails
      expect(getByText("Step 2 UI")).toBeDefined();
      expect(queryByText("Step 1 UI")).toBeNull();
    });
  });
});

useFunnel Hook代码

import { Children, ReactNode, isValidElement, useState } from "react";

export default function useFunnel<S extends string>(steps: [S, ...S[]]) {
  const [currentStep, setCurrentStep] = useState<S>(steps[0]);

  const changeStep = (step: S) => {
    setCurrentStep(step);
  };

  function Step({ children }: { name: S; children: ReactNode }) {
    return <> {children}</>;
  }

  function Funnel({ children }: { children: ReactNode }) {
    const targetStep = Children.toArray(children).find((child) => {
      if (!isValidElement(child) || child.type !== Step) {
        throw new Error(
          `${
            isValidElement(child) ? child.type : child
          } is not a <Funnel.Step> component. All component children of <Funnel> must be a <Funnel.Step>.`
        );
      }
      return child.props.name === currentStep;
    });

    return <> {targetStep}</>;
  }

  const FunnelComponent = Object.assign(Funnel, { Step });

  return [FunnelComponent, changeStep] as const;
}

原因与解决方案

核心原因

  1. 测试函数未标记为异步:使用await waitFor时,测试函数本身没有声明为async,导致await无法被正确处理,测试会在waitFor完成前就结束,断言自然失败。而直接调用waitFor时,测试框架可能同步等待其内部断言完成(这属于不可靠的侥幸通过)。
  2. act()的冗余使用:调用changeStep触发setState后,waitFor会自动处理状态更新后的渲染等待,手动用act()包裹反而可能导致状态更新与渲染时机不匹配。

修复后的测试代码

describe("useFunnel hook", () => {
  // 标记测试函数为async
  it("should successfully render the second step in the funnel", async () => {
    const { result } = renderHook(() => useFunnel(["step1", "step2"]));
    const [Funnel, changeStep] = result.current;

    const { getByText, queryByText, debug } = render(
      <React.Fragment>
        <Funnel>
          <Funnel.Step name="step1">Step 1 UI</Funnel.Step>
          <Funnel.Step name="step2">Step 2 UI</Funnel.Step>
        </Funnel>
      </React.Fragment>
    );

    // 移除act(),waitFor会自动处理状态更新等待
    changeStep("step2");

    await waitFor(() => {
      // 使用更语义化的断言方法
      expect(getByText("Step 2 UI")).toBeInTheDocument();
      expect(queryByText("Step 1 UI")).not.toBeInTheDocument();
      // 打印更新后的界面
      debug(undefined, Infinity);
    });
  });
});

额外说明

  • 优先使用toBeInTheDocument()/not.toBeInTheDocument()替代toBeDefined()/toBeNull(),这是Testing Library提供的更语义化的断言方法,需确保已导入@testing-library/jest-dom。
  • waitFor内部会自动包裹React状态更新操作,无需手动使用act(),避免手动操作导致的时机冲突。
  • 测试函数必须声明为async才能正确使用await,否则JavaScript会忽略await,导致测试提前结束,断言在组件更新前执行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 11:35:04