Библиотека тестирования React: тестирование вызова API при нажатии кнопки

Я пишу тесты для компонента 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">&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. Как мне исправить мои тесты, чтобы получить желаемый результат?

Я вижу пару странных вещей в ваших тестах, но трудно сказать, почему тестовый пример для несчастливого пути терпит неудачу, не видя фактического компонента Order. Можете ли вы включить это в свой вопрос? Кроме того, mockRejectedValue принимает объект Error, а не строку.

ivanatias 30.01.2023 02:17

@ivanatias Спасибо за информацию о mockRejectedValue; не знал этого. Просто включил компонент Order в свой вопрос.

Davy Kamanzi 30.01.2023 03:47
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
2
2
115
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я экспериментировал с упрощенным примером вашего случая, и, похоже, проблема связана с использованием хука 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" в функции настройки.

Davy Kamanzi 30.01.2023 19:08

Я обновил свой ответ, указав возможное исправление этого предупреждения, пожалуйста, проверьте.

ivanatias 30.01.2023 20:53

Другие вопросы по теме