如何模拟在使用 Jest 测试的 React 组件中进行的 API 调用
Posted
技术标签:
【中文标题】如何模拟在使用 Jest 测试的 React 组件中进行的 API 调用【英文标题】:How to mock API calls made within a React component being tested with Jest 【发布时间】:2019-09-03 14:14:20 【问题描述】:我正在尝试模拟一个将数据检索到组件中的 fetch()。
I'm using this as a model for mocking my fetches,但我无法让它工作。
我在运行测试时收到此错误:babel-plugin-jest-hoist: The module factory of 'jest.mock()' is not allowed to reference any out-of-scope variables.
有没有办法让这些函数返回模拟数据,而不是实际尝试进行真正的 API 调用?
代码
utils/getUsers.js
返回角色映射到每个用户的用户。
const getUsersWithRoles = rolesList =>
fetch(`/users`,
credentials: "include"
).then(response =>
response.json().then(d =>
const newUsersWithRoles = d.result.map(user => (
...user,
roles: rolesList.filter(role => user.roles.indexOf(role.iden) !== -1)
));
return newUsersWithRoles;
)
);
组件/UserTable.js
const UserTable = () =>
const [users, setUsers] = useState([]);
useEffect(() =>
getTableData();
, []);
const getTableData = () =>
new Promise((res, rej) => res(getRoles()))
.then(roles => getUsersWithRoles(roles))
.then(users =>
setUsers(users);
);
;
return (...)
;
组件/测试/UserTable.test.js
import "jest-dom/extend-expect";
import React from "react";
import render from "react-testing-library";
import UserTable from "../UserTable";
import getRoles as mockGetRoles from "../utils/roleUtils";
import getUsersWithRoles as mockGetUsersWithRoles from "../utils/userUtils";
const users = [
name: "Benglish",
iden: "63fea823365f1c81fad234abdf5a1f43",
roles: ["eaac4d45c3c41f449cf7c94622afacbc"]
];
const roles = [
iden: "b70e1fa11ae089b74731a628f2a9b126",
name: "senior dev"
,
iden: "eaac4d45c3c41f449cf7c94622afacbc",
name: "dev"
];
const usersWithRoles = [
name: "Benglish",
iden: "63fea823365f1c81fad234abdf5a1f43",
roles: [
iden: "eaac4d45c3c41f449cf7c94622afacbc",
name: "dev"
]
];
jest.mock("../utils/userUtils", () => (
getUsers: jest.fn(() => Promise.resolve(users))
));
jest.mock("../utils/roleUtils", () => (
getRolesWithUsers: jest.fn(() => Promise.resolve(usersWithRoles)),
getRoles: jest.fn(() => Promise.resolve(roles))
));
test("<UserTable/> show users", () =>
const queryByText = render(<UserTable />);
expect(queryByText("Billy")).toBeTruthy();
);
【问题讨论】:
【参考方案1】:默认情况下,jest.mock
调用由 babel-jest
提升...
...这意味着它们在测试文件中的任何其他内容之前运行,因此在测试文件中声明的任何变量都不会在范围内。
这就是为什么传递给jest.mock
的模块工厂不能引用自身之外的任何东西。
一种选择是将数据移入模块工厂,如下所示:
jest.mock("../utils/userUtils", () =>
const users = [ /* mock users data */ ];
return
getUsers: jest.fn(() => Promise.resolve(users))
;
);
jest.mock("../utils/roleUtils", () =>
const roles = [ /* mock roles data */ ];
const usersWithRoles = [ /* mock usersWithRoles data */ ];
return
getRolesWithUsers: jest.fn(() => Promise.resolve(usersWithRoles)),
getRoles: jest.fn(() => Promise.resolve(roles))
;
);
另一种选择是使用 jest.spyOn
模拟函数:
import * as userUtils from '../utils/userUtils';
import * as roleUtils from '../utils/roleUtils';
const users = [ /* mock users data */ ];
const roles = [ /* mock roles data */ ];
const usersWithRoles = [ /* mock usersWithRoles data */ ];
const mockGetUsers = jest.spyOn(userUtils, 'getUsers');
mockGetUsers.mockResolvedValue(users);
const mockGetRolesWithUsers = jest.spyOn(roleUtils, 'getRolesWithUsers');
mockGetRolesWithUsers.mockResolvedValue(usersWithRoles);
const mockGetRoles = jest.spyOn(roleUtils, 'getRoles');
mockGetRoles.mockResolvedValue(roles);
另一种选择是自动模拟模块:
import * as userUtils from '../utils/userUtils';
import * as roleUtils from '../utils/roleUtils';
jest.mock('../utils/userUtils');
jest.mock('../utils/roleUtils');
const users = [ /* mock users data */ ];
const roles = [ /* mock roles data */ ];
const usersWithRoles = [ /* mock usersWithRoles data */ ];
userUtils.getUsers.mockResolvedValue(users);
roleUtils.getRolesWithUsers.mockResolvedValue(usersWithRoles);
roleUtils.getRoles.mockResolvedValue(roles);
...并将模拟的响应添加到空的模拟函数中。
【讨论】:
嗯。这似乎对我不起作用。是因为我试图模拟 npm 包吗?我使用自动生成的 TS 客户端来处理我的一个 API,该 API 在被测组件中使用 @SteveBoniface 所有这些策略都适用于用户模块和节点模块 (npm),所以这可能是一个不同的问题 好的。好吧,FWIW,我从同一个模块导入了几个不同的东西。两者都在被测组件中使用,在这些类上调用它们的构造函数和方法。我已经做得够多了,可以为这两个依赖项返回一个 mockConstructor,但是我如何才能真正模拟这些类上的函数实现呢?【参考方案2】:不要模拟进行 API 调用的工具;存根服务器响应。下面是我将如何使用名为 nock 的 HTTP 拦截器重写您的测试。
import "jest-dom/extend-expect";
import React from "react";
import render, waitFor from "react-testing-library";
import UserTable from "../UserTable";
const users = [
name: "Benglish",
iden: "63fea823365f1c81fad234abdf5a1f43",
roles: ["eaac4d45c3c41f449cf7c94622afacbc"]
];
const roles = [
iden: "b70e1fa11ae089b74731a628f2a9b126",
name: "senior dev"
,
iden: "eaac4d45c3c41f449cf7c94622afacbc",
name: "dev"
];
const usersWithRoles = [
name: "Benglish",
iden: "63fea823365f1c81fad234abdf5a1f43",
roles: [
iden: "eaac4d45c3c41f449cf7c94622afacbc",
name: "dev"
]
];
describe("<UserTable/>", () =>
it("shows users", async () => // <-- Async to let nock kick over resolved promise
nock(`$server`)
.get('/users')
.reply(200,
data: users
)
.get('/usersWithRoles')
.reply(200,
data: usersWithRoles
)
.get('/roles')
.reply(200,
data: roles
);
const queryByText = render(<UserTable />);
await waitFor(() => expect(queryByText("Billy")).toBeTruthy()); // <-- Is this supposed to be "Benglish"?
);
);
现在您的测试套件不知道您是如何获取数据的,并且您不必维护复杂的模拟。查看Testing Components that make API calls 进行更深入的了解。
【讨论】:
【参考方案3】:您需要将模拟范围中使用的变量重命名为前缀为mock
(例如mockUsers
)。
Jest 做了一些提升魔法,允许您用模拟替换导入的模块,但似乎确实需要这些特殊的变量名前缀来做它的事情。
【讨论】:
以上是关于如何模拟在使用 Jest 测试的 React 组件中进行的 API 调用的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 jest 模拟 react-router-dom 的 BrowserRouter
React Jest:如何模拟返回数据的异步 API 调用?
当模拟点击调用调用 promise 的函数时,使用 React 的 Jest 和 Enzyme 进行测试