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

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)。

错误原因分析

  1. 组件挂载后的异步状态更新未被act包裹:在beforeEach中调用render后,组件会触发fetch请求,当fetch的Promise resolve后,组件会更新状态。这个状态更新发生在测试框架的act()上下文之外,因为render是同步执行的,后续的异步状态更新没有被包裹在act中,导致报错。
  2. 点击操作错误地放在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 20:15:43