模拟是一种测试替身,它取代了代码库的一部分,使测试更容易。一个经常被模拟的代码的例子是一个网络API调用。有几个原因可以解释为什么模拟网络API调用是有用的。
- 为了确保网络API调用的响应是一致的--真正的网络API背后的数据可能会随着时间的推移而改变,导致真正的网络API响应不同。
- 为了加快测试的速度--网络API请求很慢。
- 如果网络API是第三方付费服务,模拟它可以减少成本。
这篇文章介绍了如何在React组件测试中模拟一个进行API调用的函数。

要测试的组件
下面是要测试的组件,它渲染了一个从 "星球大战 "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中找到,链接如下: