React测试出现not wrapped in act错误,求排查原因(附代码)
解决React测试中的"not wrapped in act"错误
问题场景
函数组件中使用fetch获取数据并更新状态,测试时Mock了fetch返回假数据,编写了3个测试用例:
- 验证
fetch是否被调用(断言包裹在waitFor内) - 验证数据是否正确展示(断言包裹在
waitFor内) - 验证点击元素是否跳转至假URL(点击操作与断言均包裹在
waitFor内)
所有测试均通过,但运行时出现3个not wrapped in act错误。以下是测试代码:
const savedLocation = window.location; jest.spyOn(window, 'fetch'); const fakeData = { "articles" : [{ title: "title", url: "url", desc: "desc" }]}; beforeEach(() => { delete window.location; window.location = { assign: jest.fn() }; window.fetch.mockResolvedValueOnce({ json: () => fakeData }); render(<Router><Home /></Router>); }); afterEach(() => { window.location = savedLocation; }); it("fetch is called", async () => { await waitFor(() => { expect(window.fetch).toHaveBeenCalledTimes(1); }); }); it("article is displayed with correct data", async () => { await waitFor(() => { expect(screen.getByRole('heading')).toHaveTextContent('title'); expect(screen.getByTestId('desc')).toHaveTextContent('desc'); }); }); it("click on article calls window.location.assign with fake url", async () => { await waitFor(() => { userEvent.click(screen.getByTestId('artCont')); expect(window.location.assign).toHaveBeenCalledWith(fakeData.articles[0].url); }); });
注:
artCont是展示获取数据标题和描述的div容器,点击该div会更改URL。另外,删除了window.location以验证assign函数(新版本无法直接mock)。
错误原因分析
- 组件挂载后的异步状态更新未被act包裹:在
beforeEach中调用render后,组件会触发fetch请求,当fetch的Promise resolve后,组件会更新状态。这个状态更新发生在测试框架的act()上下文之外,因为render是同步执行的,后续的异步状态更新没有被包裹在act中,导致报错。 - 点击操作错误地放在waitFor回调内:
userEvent.click本身是异步操作,需要用await直接等待完成,而不是放在waitFor的回调函数里。waitFor的作用是等待断言条件变为真,而非执行异步操作。
修复方案
1. 在beforeEach中等待初始异步操作完成
将beforeEach中的render改为等待组件的初始异步状态更新完成,确保所有状态变化都在act上下文内:
beforeEach(async () => { delete window.location; window.location = { assign: jest.fn() }; // 每次测试前重新spy fetch jest.spyOn(window, 'fetch').mockResolvedValueOnce({ json: () => fakeData }); render(<Router><Home /></Router>); // 等待初始fetch执行完成 await waitFor(() => expect(window.fetch).toHaveBeenCalledTimes(1)); });
2. 调整测试用例中的异步操作处理
- 前两个测试中,因为初始异步操作已经在
beforeEach中完成,可直接断言无需再用waitFor; - 第三个测试中,将
userEvent.click移出waitFor并直接await:
it("click on article calls window.location.assign with fake url", async () => { const articleContainer = screen.getByTestId('artCont'); await userEvent.click(articleContainer); expect(window.location.assign).toHaveBeenCalledWith(fakeData.articles[0].url); });
3. 清理Mock避免跨测试污染
在afterEach中恢复所有Mock,确保测试用例之间相互独立:
afterEach(() => { window.location = savedLocation; jest.restoreAllMocks(); });
最终修复后的完整测试代码
const savedLocation = window.location; const fakeData = { "articles" : [{ title: "title", url: "url", desc: "desc" }]}; beforeEach(async () => { delete window.location; window.location = { assign: jest.fn() }; jest.spyOn(window, 'fetch').mockResolvedValueOnce({ json: () => fakeData }); render(<Router><Home /></Router>); await waitFor(() => expect(window.fetch).toHaveBeenCalledTimes(1)); }); afterEach(() => { window.location = savedLocation; jest.restoreAllMocks(); }); it("fetch is called", async () => { expect(window.fetch).toHaveBeenCalledTimes(1); }); it("article is displayed with correct data", async () => { expect(screen.getByRole('heading')).toHaveTextContent('title'); expect(screen.getByTestId('desc')).toHaveTextContent('desc'); }); it("click on article calls window.location.assign with fake url", async () => { const articleContainer = screen.getByTestId('artCont'); await userEvent.click(articleContainer); expect(window.location.assign).toHaveBeenCalledWith(fakeData.articles[0].url); });
内容的提问来源于stack exchange,提问作者ptjr
相关产品推荐
相关产品推荐

