如何在Jest中为React和Typescript应用模拟一个函数

114 阅读3分钟

模拟是一种测试替身,它取代了代码库的一部分,使测试更容易。一个经常被模拟的代码的例子是一个网络API调用。有几个原因可以解释为什么模拟网络API调用是有用的。

  • 为了确保网络API调用的响应是一致的--真正的网络API背后的数据可能会随着时间的推移而改变,导致真正的网络API响应不同。
  • 为了加快测试的速度--网络API请求很慢。
  • 如果网络API是第三方付费服务,模拟它可以减少成本。

这篇文章介绍了如何在React组件测试中模拟一个进行API调用的函数。

Mock a function in Jest for a React and Typescript app

照片:Jørgen HålandonUnsplash

要测试的组件

下面是要测试的组件,它渲染了一个从 "星球大战 "API请求的角色名称:

export function Hello({ id }: Props) {
  const [character, setCharacter] = React.useState<undefined | string>(
    undefined
  );

  React.useEffect(() => {
    getCharacter(id).then((c) => setCharacter(c));
  }, [id]);

  if (character === undefined) {
    return null;
  }
  return <p>Hello {character}</p>;
}

这就是我们的测试:

test("Should render character name", async () => {
  render(<Hello id={1} />);
  expect(await screen.findByText(/Bob/)).toBeInTheDocument();
});

但测试失败了,因为该组件显示的是天行者卢克。😞

我们可以改变期望值以找到Luke Skywalker,但这将测试与数据结合起来,使其变得很脆弱,因为数据可能会随着时间的推移而改变。

我们将模拟getCharacter ,让它返回"Bob" ,以解决这个问题。

第一次尝试

这里是一个天真的尝试:

import { getCharacter } from "./data";

test("Should render character name", async () => {
  const safe = getCharacter;

  // 💥 Cannot assign to 'getCharacter' because it is an import
  getCharacter = (id) => {
    return new Promise((resolve) => resolve("Bob"));
  };

  render(<Hello id={1} />);

  expect(await screen.findByText(/Bob/)).toBeInTheDocument();

  // 💥 Cannot assign to 'getCharacter' because it is an import
  getCharacter = safe;
});

但这还远远不够完美。主要的问题是,当一个新的值被分配给导入的函数时,它会出错。

第二次尝试

import * as name 语法在一个对象结构中导入整个模块。如果我们这样导入的话,也许我们就能从一个模块中模拟出一个函数?

如果我们按如下方式导入getCharacter :

import * as data from "./data";

......getCharacter 可以被访问为data.getCharacter :

让我们重构测试,使用这种方法:

import * as data from "./data";
test("Should render character name", async () => {
  const safe = data.getCharacter;

  // 💥 Cannot assign to 'getCharacter' because it is a read-only property
  data.getCharacter = (id) => {    return new Promise((resolve) => resolve("Bob"));
  };

  render(<Hello id={1} />);

  expect(await screen.findByText(/Bob/)).toBeInTheDocument();

  // 💥 Cannot assign to 'getCharacter' because it is a read-only property
  data.getCharacter = safe;});

这还是不行的😞

然而,我们感觉已经向前迈进了一步,因为它并不是说我们不能模拟导入,而是说我们不能模拟getCharacter ,因为它是只读的。

使用jest.spyOn

Jest有一个 spyOn函数,它可以解决这个问题,使测试变得更好。

jest.spyOn 允许对一个对象中的方法进行模拟,语法是:

jest.spyOn(object, methodName);

它也有一个mockResolvedValue 方法来提供解决的返回值。

让我们重构测试,使用这种方法:

test("Should render character name", async () => {
  const mock = jest.spyOn(data, "getCharacter").mockResolvedValue("Bob");

  render(<Hello id={1} />);

  expect(await screen.findByText(/Bob/)).toBeInTheDocument();

  mock.mockRestore();
});

请注意,spyOn 也有一个mockRestore 方法,在测试结束时恢复原来的实现。

该测试现在可以工作了!

但我们可以做得更好一些。我们可以使用toHaveBeenCalled* 的期望来验证模拟被调用:

test("Should render character name", async () => {
  const mock = jest.spyOn(data, "getCharacter").mockResolvedValue("Bob");

  render(<Hello id={1} />);

  expect(await screen.findByText(/Bob/)).toBeInTheDocument();

  expect(mock).toHaveBeenCalledTimes(1);
  expect(mock).toHaveBeenCalledWith(1);

  mock.mockRestore();
});

很好!☺️

这篇文章的代码可以在Codesandbox中找到,链接如下:

🏃玩转代码