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

使用Vitest测试React时useState不更新及act()报错问题求助

React + Vitest测试:act()包裹点击导致测试失效,不包裹则日志报错的解决方法

在编写React组件的Vitest测试时碰到了这个问题:Vitest日志提示必须把按钮点击操作放进act()中,但手动添加act()后测试直接跑失败,不加又会出现act相关的日志错误。下面是相关代码和可行的解决办法:


业务函数代码

const handleSendJobsAssigned = async (event: React.MouseEvent) => {
  setAssingingJobsLoading(true)
  let counter = 0
  let failed = false

  try {
    for (const job of assignedJobs) {
      await api.put(url, {
        status: "test",
        assignee: 1
      })
      removeAssignedJob(1)
      counter += 1
    }
  } catch (e) {
    failed = true
  } finally {
    showToast(counter, failed)
    setAssingingJobsLoading(false)
    clearJobs()
    refreshSelectedTab()
  }
}

两种失效的测试写法

1. 嵌套waitFor的act()写法

await act( async () => {
  fireEvent.click(assignButton);

  //on click function
  await vi.waitFor(() => {
    expect(assignButton).toBeDisabled();
    expect(cancelButton).toBeDisabled();
  })

  //after click function
  await vi.waitFor(() => {
    expect(assignButton).toBeEnabled();
    expect(cancelButton).toBeEnabled();
  });
})

2. 单独act()包裹点击的写法

await act( async () => {
  fireEvent.click(assignButton);
})

//on click function
await vi.waitFor(() => {
  expect(assignButton).toBeDisabled();
  expect(cancelButton).toBeDisabled();
})

//after click function
await vi.waitFor(() => {
  expect(assignButton).toBeEnabled();
  expect(cancelButton).toBeEnabled();
});

可运行但有日志错误的测试写法

fireEvent.click(assignButton);

//on click function
await vi.waitFor(() => {
  expect(assignButton).toBeDisabled();
  expect(cancelButton).toBeDisabled();
})

//after click function
await vi.waitFor(() => {
  expect(assignButton).toBeEnabled();
  expect(cancelButton).toBeEnabled();
});

可行解决方法

核心思路:别手动写act(),正确mock异步操作

Vitest和React Testing Library已经自动处理了绝大多数act()的场景,手动加act()反而会打乱React的更新时序,导致断言时机错误。

具体步骤

  1. 先mock掉异步API请求
    把业务代码里的api.put用Vitest的mock方法替换,让它快速返回结果,避免真实网络请求拖慢测试或导致时序问题:

    // 在测试用例的beforeEach或测试开头添加
    vi.spyOn(api, 'put').mockResolvedValue({});
    
  2. 调整测试逻辑,分阶段断言
    不用手动包裹act(),直接点击按钮后分两次断言:先断言加载中的状态,再断言加载完成后的状态:

    // mock API请求
    vi.spyOn(api, 'put').mockResolvedValue({});
    
    // 直接点击按钮,不用act()包裹
    fireEvent.click(assignButton);
    
    // 断言加载中:按钮禁用
    await vi.waitFor(() => {
      expect(assignButton).toBeDisabled();
      expect(cancelButton).toBeDisabled();
    });
    
    // 等待所有异步操作完成,断言加载结束:按钮启用
    await vi.waitFor(() => {
      // 可选:验证API被调用的次数是否符合预期
      expect(api.put).toHaveBeenCalledTimes(assignedJobs.length);
      expect(assignButton).toBeEnabled();
      expect(cancelButton).toBeEnabled();
    });
    

为什么之前的act()写法会失效?

  • 第一种嵌套waitFor的act():act()内部的异步操作会被React批量处理,但waitFor本身已经在跟踪React的异步更新,嵌套会导致更新队列混乱,断言的时机完全不对。
  • 第二种单独包裹点击的act():act()只包裹了点击瞬间的同步状态更新,但后续API请求触发的异步状态更新不在act()范围内。而实际上fireEvent已经自动帮你处理了act()的包裹,手动加反而会中断测试对异步流程的跟踪。

额外优化:用userEvent代替fireEvent

如果还是偶尔出现act警告,可以换成React Testing Library的userEvent,它更贴近真实用户的交互行为,并且完全自动处理act()的包裹:

import { userEvent } from '@testing-library/react';

// ...

// 用await包裹userEvent.click,因为它是异步的
await userEvent.click(assignButton);

// 后续断言和之前一致
await vi.waitFor(() => {
  expect(assignButton).toBeDisabled();
  expect(cancelButton).toBeDisabled();
});

await vi.waitFor(() => {
  expect(assignButton).toBeEnabled();
  expect(cancelButton).toBeEnabled();
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 06:17:09