Я пишу тесты для компонента React Order
, который отображает заказ пользователя и позволяет пользователю просматривать элементы заказа, нажимая кнопку «Просмотр элементов», которая запускает вызов API.
import { useState, useEffect } from "react";
import Skeleton from "react-loading-skeleton";
import { Customer } from "../../../api/Server";
import capitalise from "../../../util/capitalise";
import renderOrderTime from "../../../util/renderOrderTime";
const Order = props => {
// Destructure props and details
const { details, windowWidth, iconHeight, cancelOrder } = props;
const { id, createdAt, status } = details;
// Define server
const Server = Customer.orders;
// Define order cancel icon
const OrderCancel = (
<svg className = "iconOrderCancel" width = {iconHeight} height = {iconHeight} viewBox = "0 0 24 24">
<path className = "pathOrderCancel" style = {{ fill:"#ffffff" }} d = "M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5 15.538l-3.592-3.548 3.546-3.587-1.416-1.403-3.545 3.589-3.588-3.543-1.405 1.405 3.593 3.552-3.547 3.592 1.405 1.405 3.555-3.596 3.591 3.55 1.403-1.416z"/>
</svg>
)
// Set state and define fetch function
const [items, setItems] = useState([]);
const [fetchItems, setFetchItems] = useState(false);
const [isLoadingItems, setIsLoadingItems] = useState(false);
const [error, setError] = useState(null);
const fetchOrderItems = async() => {
setIsLoadingItems(true);
try {
let order = await Server.getOrders(id);
setItems(order.items);
setIsLoadingItems(false);
} catch (err) {
setError(true);
console.info(err);
}
}
useEffect(() => {
if (items.length === 0 && fetchItems) fetchOrderItems();
// eslint-disable-next-line
}, [fetchItems]);
// Define function to view order items
const viewItems = e => {
e.preventDefault();
setFetchItems(fetchItems ? false : true);
const items = document.getElementById(`items-${id}`);
items.classList.toggle("show");
}
// RENDERING
// Order items
const renderItems = () => {
// Return error message if error
if (error) return <p className = "error">An error occurred loading order items. Kindly refresh the page and try again.</p>;
// Return skeleton if loading items
if (isLoadingItems) return <Skeleton containerTestId = "order-items-loading" />;
// Get total cost of order items
let total = items.map(({ totalCost }) => totalCost).reduce((a, b) => a + b, 0);
// Get order items
let list = items.map(({ productId, name, quantity, totalCost}, i) => {
return (
<div key = {i} className = "item" id = {`order-${id}-item-${productId}`}>
<p className = "name">
<span>{name}</span><span className = "times">×</span><span className = "quantity">{quantity}</span>
</p>
<p className = "price">
<span className = "currency">Ksh</span><span>{totalCost}</span>
</p>
</div>
)
});
// Return order items
return (
<div id = {`items-${id}`} className = {`items${items.length === 0 ? null : " show"}`} data-testid = "order-items">
{list}
{
items.length === 0 ? null : (
<div className = "item total" id = {`order-${id}-total`}>
<p className = "name">Total</p>
<p className = "price">
<span className = "currency">Ksh</span><span>{total}</span>
</p>
</div>
)
}
</div>
)
};
// Component
return (
<>
<div className = "order-body">
<div className = "info">
<p className = "id">#{id}</p>
<p className = "time">{renderOrderTime(createdAt)}</p>
<button className = "view-items" onClick = {viewItems}>{ fetchItems ? "Hide items" : "View items"}</button>
{renderItems()}
</div>
</div>
<div className = "order-footer">
<p className = "status" id = {`order-${id}-status`}>{capitalise(status)}</p>
{status === "pending" ? <button className = "cancel-order" onClick = {cancelOrder}>{windowWidth > 991 ? "Cancel order" : OrderCancel}</button> : null}
</div>
</>
)
}
export default Order;
Ниже приведены тесты, для которых я пишу Order
. У меня возникли проблемы с написанием тестов для вызова API.
import { render, fireEvent, screen } from "@testing-library/react";
import { act } from "react-dom/test-utils";
import Order from "../../../components/Primary/Orders/Order";
import { Customer } from "../../../api/Server";
import { orders } from "../../util/dataMock";
// Define server
const Server = Customer.orders;
// Define tests
describe("Order", () => {
describe("View items button", () => {
const mockGetOrders = jest.spyOn(Server, "getOrders");
beforeEach(() => {
const { getAllByRole } = render(<Order details = {orders[2]} />);
let button = getAllByRole("button")[0];
fireEvent.click(button);
});
test("triggers API call when clicked", () => {
expect(mockGetOrders).toBeCalled();
});
test("renders loading skeleton during API call", () => {
let skeleton = screen.getByTestId("order-items-loading");
expect(skeleton).toBeInTheDocument();
});
///--- PROBLEMATIC TEST ---///
test("renders error message if API call fails", async() => {
await act(async() => {
mockGetOrders.mockRejectedValue("Error: An unknown error occurred. Kindly try again.");
});
// let error = await screen.findByText("An error occurred loading order items. Kindly refresh the page and try again.");
// expect(error).toBeInTheDocument();
});
});
test("calls cancelOrder when button is clicked", () => {
const clickMock = jest.fn();
const { getAllByRole } = render(<Order details = {orders[2]} cancelOrder = {clickMock} />);
let button = getAllByRole("button")[1];
fireEvent.click(button);
expect(clickMock).toBeCalled();
});
});
В тесте, который я отметил как проблемный, я ожидаю, что имитированное отклоненное значение Error: An unknown error occurred. Kindly try again.
будет зарегистрировано в консоли, но вместо этого я получаю TypeError: Cannot read properties of undefined (reading 'items')
. Это указывает на то, что мой фиктивный вызов API не работает должным образом, и компонент пытается воздействовать на массив элементов, которые он должен получить от API. Как мне исправить мои тесты, чтобы получить желаемый результат?
@ivanatias Спасибо за информацию о mockRejectedValue
; не знал этого. Просто включил компонент Order
в свой вопрос.
Я экспериментировал с упрощенным примером вашего случая, и, похоже, проблема связана с использованием хука beforeEach
, в котором вы визуализируете компонент и нажимаете кнопку перед каждым тестом. Это не очень хорошая идея, когда дело доходит до RTL, на самом деле для этого есть правило Eslint.
Чтобы не повторяться в каждом тесте, вы можете использовать функцию setup
для выполнения следующих действий в каждом из тестов:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Order from "../../../components/Primary/Orders/Order";
import { Customer } from "../../../api/Server";
import { orders } from "../../util/dataMock";
// Define server
const Server = Customer.orders;
// Setup function
const setup = () => {
render(<Order details = {orders[2]} />);
const button = screen.getAllByRole("button")[0];
userEvent.click(button);
}
// Define tests
describe("Order", () => {
describe("View items button", () => {
const mockGetOrders = jest.spyOn(Server, "getOrders");
test("triggers API call when clicked", () => {
setup()
expect(mockGetOrders).toBeCalled();
});
test("renders loading skeleton during API call", async () => {
setup()
const skeleton = await screen.findByTestId("order-items-loading");
expect(skeleton).toBeInTheDocument();
});
test("renders error message if API call fails", async () => {
mockGetOrders.mockRejectedValue(new Error("An unknown error occurred. Kindly try again."));
setup()
const error = await screen.findByText("An error occurred loading order items. Kindly refresh the page and try again.");
expect(error).toBeInTheDocument();
});
});
test("calls cancelOrder when button is clicked", () => {
const clickMock = jest.fn();
render(<Order details = {orders[2]} cancelOrder = {clickMock} />);
const button = screen.getAllByRole("button")[1];
userEvent.click(button);
expect(clickMock).toBeCalled();
});
});
Я также внес некоторые незначительные изменения, такие как: использование userEvent
вместо fireEvent
, использование screen
вместо деструктуризации функций запроса из результата render
и т. д. Все эти изменения рекомендованы создателем библиотеки.
Кроме того, в функции fetchOrderItems
вы должны установить флаг загрузки на false
на случай возникновения исключения.
Примечание. Предупреждение act
может исходить от тестов №1 и №2. Вероятно, это связано с тем, что тестовые примеры заканчиваются до завершения всех обновлений состояния компонента (например, переключение состояния загрузки после нажатия кнопки выборки). Не рекомендуется использовать act
в RTL, вместо этого вы можете изменить эти тесты и использовать утилиту waitForElementToBeRemoved
из RTL. Это по-прежнему будет удовлетворять тесту № 2, в котором вы проверяете наличие загрузочного скелета, поскольку он все равно выдаст исключение, если не сможет найти этот загрузочный интерфейс:
test("triggers API call when clicked", async () => {
setup()
await waitForElementToBeRemoved(() => screen.queryByTestId("order-items-loading"))
expect(mockGetOrders).toBeCalled();
});
test("renders loading skeleton during API call", async () => {
setup()
await waitForElementToBeRemoved(() => screen.queryByTestId("order-items-loading"))
});
Дайте мне знать, если это работает для вас.
Спасибо! Это работает, но я получаю старый добрый Warning: An update to Order inside a test was not wrapped in act(...)
на третьем View items button
тесте. Перенос первых двух строк в act()
вызывает TestingLibraryElementError: Unable to find an accessible element with the role "button"
в функции настройки.
Я обновил свой ответ, указав возможное исправление этого предупреждения, пожалуйста, проверьте.
Я вижу пару странных вещей в ваших тестах, но трудно сказать, почему тестовый пример для несчастливого пути терпит неудачу, не видя фактического компонента
Order
. Можете ли вы включить это в свой вопрос? Кроме того,mockRejectedValue
принимает объектError
, а не строку.