#javascript #reactjs #jestjs #react-testing-library
#javascript #reactjs #jestjs #react-testing-library
Вопрос:
Я пытаюсь имитировать fetch(), который извлекает данные в компонент.
Я использую это как модель для издевательства над моими выборками, но у меня возникают проблемы с его работой.
Я получаю эту ошибку при запуске своих тестов: 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;
})
);
component/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 (...)
};
component/tests/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);
… и добавьте смоделированный ответ к пустым макетным функциям.
Комментарии:
1. Хм. Похоже, это не сработало для меня. Это потому, что я пытаюсь имитировать пакеты npm? Я использую автоматически сгенерированный клиент TS для работы с одним из моих API, который используется в тестируемом компоненте
2. @SteveBoniface все эти стратегии работают как для пользовательских модулей, так и для модулей узлов (npm), так что, скорее всего, это другая проблема
3. Хорошо. Ну, черт возьми, у меня есть пара разных вещей, которые я импортирую из одного и того же модуля. Оба из которых используются в тестируемом компоненте, вызывая их конструкторы и методы в этих классах. Я продвинулся достаточно далеко, чтобы я мог вернуть mockConstructor для этих двух зависимостей, но как я могу на самом деле имитировать реализации функций в этих классах?
Ответ №2:
Не имитируйте инструмент, выполняющий вызовы API; заглушите ответы сервера. Вот как я бы переписал ваш тест, используя HTTP-перехватчик под названием nock.
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"?
});
});
Теперь ваш набор тестов не знает, как вы получаете данные, и вам не нужно поддерживать сложные макеты. Ознакомьтесь с тестированием компонентов, которые выполняют вызовы API, для более глубокого погружения.
Комментарии:
1. Что
${server}
необходимо при выполнении тестов с помощью Create React App?
Ответ №3:
В наши дни макет Service Worker выглядит как хороший вариант, чтобы «имитировать, перехватывая запросы на сетевом уровне».
Ответ №4:
Вам необходимо переименовать переменные, используемые в области макетов, с префиксом mock
(например mockUsers
).
Jest выполняет некоторую магию подъема, позволяющую вам заменять импортированные модули на mocks, но, похоже, для этого требуются эти специальные префиксы имен переменных.