使用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的更新时序,导致断言时机错误。
具体步骤
先mock掉异步API请求
把业务代码里的api.put用Vitest的mock方法替换,让它快速返回结果,避免真实网络请求拖慢测试或导致时序问题:// 在测试用例的beforeEach或测试开头添加 vi.spyOn(api, 'put').mockResolvedValue({});调整测试逻辑,分阶段断言
不用手动包裹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
相关产品推荐
相关产品推荐

