如何在测试React应用时模拟Fetch请求?

9

我想测试一个小的React Web应用,其中使用全局fetch方法。

我尝试通过以下方式模拟fetch

global.fetch = jest.spyOn(global, 'fetch').mockImplementation(endpoint =>
  Promise.resolve({
    json: () => Promise.resolve(mockResponse)
  })
);

...但是模拟似乎被忽略了,而内置的fetch被使用:Error: connect ECONNREFUSED 127.0.0.1:80 ...看起来像是对内置的fetch进行调用失败。

然后我尝试使用jest.fn代替jest.spyOn

global.fetch = jest.fn(endpoint =>
  Promise.resolve({
    json: () => Promise.resolve(mockResponse)
  })
);

我惊讶地发现出现了不同的错误。现在似乎会考虑模拟,但同时却没有正常工作:

    TypeError: Cannot read property 'then' of undefined

       8 |     this.updateTypes = this.props.updateTypes;
       9 |     this.updateTimeline = this.props.updateTimeline;
    > 10 |     fetch('/timeline/tags')
         |     ^
      11 |       .then(res => res.json())
      12 |       .then(tags => tags.map(tag => <option value={tag} key={tag} />))
      13 |       .then(options => options.sort((a, b) => a.key.localeCompare(b.key)))

我觉得 Jest 和 React Testing Library 的文档有点令人困惑,说实话。我所做的可能存在问题是什么?

编辑

我正在尝试测试的 React 组件名为“App”,使用 Create React App 生成,并更改以包含对 fetch 的调用。我可以很高兴地提供此组件的代码,但我认为问题在于测试。

在我的 App.test.js 文件开头,我首先 import React from 'react';,然后 import { render, fireEvent, waitFor, screen } from '@testing-library/react';,最后 import App from './App';。随后,我尝试以我描述过的一种方式模拟 fetch,然后声明以下测试:

test('renders a list of items, upon request', async () => {
  const app = render(<App />);

  fireEvent.click(screen.getByText('Update'));

  await waitFor(() => screen.getByRole('list'));

  expect(screen.getByRole('list')).toBeInTheDocument();
  expect(screen.getByRole('list')).toHaveClass('Timeline');
});

最后,我用 global.fetch.mockRestore(); 结束了我的测试文件。


你看过这篇文章吗?https://kentcdodds.com/blog/stop-mocking-fetch/ - Red Baron
请提供一种复制问题的方法。您的模拟存在某些问题,这些问题无法通过您发布的片段进行解释。但是似乎忽略了模拟,而使用了内置的fetch - 这应该会导致错误,因为JSDOM不提供fetch,并且在缺少属性的情况下调用jest.spyOn是不允许的。我看不到第二个模拟单独出现无法读取未定义的“then”属性错误的可能性,除非错误指的是另一行,如果源映射出错,则可能发生这种情况。 - Estus Flask
@RedBaron 顺便提一下,这篇文章涉及到的是集成测试而不是单元测试,应该谨慎对待。 - Estus Flask
@EstusFlask 我在我的问题中添加了更多信息。如果我添加的不够(例如,如果我应该添加整个App组件的代码),请让我知道,我会编辑我的问题。 - Giorgio
不好意思,这还不够。我已经尝试解释了如何进行模拟。如果答案没有帮助,那么问题非常特定于您的项目,请提供重现问题的方法 - 例如存储库等。 - Estus Flask
1个回答

19

出现 ECONNREFUSED 错误而不是 fetch未定义 的意思是,fetch 已经被polyfill了。它不是 JSDOM 的一部分,也不是 Jest 本身提供的polyfill,而是特定于当前设置的。在这种情况下,polyfill由create-react-app提供。

最好使用jest.spyOn来模拟现有的全局函数,而不是将它们指定为global属性,这样可以让Jest进行清理。像 global.fetch = jest.spyOn(global, 'fetch') 这样的事情永远不应该被做,因为这会防止fetch被还原。这可能会导致看似正确的模拟函数出现 TypeError:无法读取未定义属性'then' 错误。

正确且安全地模拟全局变量的方法是在每个测试之前模拟它们,并在每个测试之后恢复:

beforeEach(() => {
  jest.spyOn(global, 'fetch').mockResolvedValue({
    json: jest.fn().mockResolvedValue(mockResponse)
  })
});

afterEach(() => {
  jest.restoreAllMocks();
});

为了使模拟工作正确,global.fetch 不应进行其他修改。

恢复模拟和间谍的首选方式是使用配置选项而不是使用jest.restoreAllMocks,因为不这样做可能会导致意外的测试交叉污染,这是永远不可取的。

出现TypeError: Cannot read property 'then' of undefined错误的另一个原因是Jest错误地指向fetch行,而实际错误是指另一行。 如果源映射没有正确工作,则可能会发生这种情况。如果fetch正确模拟,并且在同一组件中有其他then,那么这是出错的合理解释。


1
感谢您提供详细的答案。我的测试问题最终被证明是初学者的错误:我没有使用 beforeEachafterEach - Giorgio

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接