fireEvent проти @testing-library/user-event.Важливість тестування для забезпечення стабільності систем. Вивчення Jest та React Testing Library для тестування компонентів з точки зору користувача. Огляд інструментів для наскрізного тестування (Playwright, Cypress) та їх роль у CI/CD процесах.
Програмне забезпечення будь-якого рівня без покриття автоматизованими тестами вважається “спадщиною” (Legacy-кодом) вже в момент його написання. У сучасному Enterprise-середовищі зміна хоча б одного рядка коду може призвести до ефекту метелика, обваливши функціональність у непов’язаних модулях (Регресія). Ручне тестування (QA-інженерами) кожної гілки логіки під час щоденних релізів фізично неможливе. Тому інженери 4-го курсу зобов’язані володіти методологією створення автоматизованих “запобіжників” — тестів. У React-екосистемі наразі домінує симбіоз інструментів Jest (або Vitest) та React Testing Library (RTL). Дана лекція розкриває архітектурні принципи написання тестів, які орієнтуються не на внутрішній код, а на реальний користувацький досвід (UX).
Індустрія програмної інженерії поділяє тести на три основні категорії (“Піраміда тестування”, Мартін Фаулер):
formatDate або кнопка <Button/>). Виконуються за мілісекунди.<Modal/>, якщо в ньому натиснути на <CloseButton/>).У цій лекції ми сфокусуємося на Модульному та Інтеграційному рівнях.
Для запуску тестів у JavaScript потрібне спеціальне середовище виконання (Test Runner), яке надає глобальні змінні типу describe, it, test, expect.
Найпопулярнішими є Jest (стандарт де-факто для React) та Vitest (швидка сучасна альтернатива для проектів на базі збирача Vite).
Анатомія базового тесту на Jest:
// math.js
export const sum = (a, b) => a + b;
// math.test.js
import { sum } from "./math";
// Блок об'єднання тестів (Test Suite)
describe("Математичний модуль", () => {
// Конкретний тестовий випадок (Test Case)
it("повинен коректно додавати два додатні числа", () => {
// 1. Arrange (Підготовка даних)
const a = 5,
b = 10;
// 2. Act (Виконання)
const result = sum(a, b);
// 3. Assert (Перевірка очікувань - Assertion)
expect(result).toBe(15);
});
});
Якщо sum поверне 16, Jest викине червону помилку в консоль терміналу, і CI/CD сервер відхилить злиття коду (Git Push відхилено).
Історично компоненти тестували бібліотекою Enzyme, яка перевіряла “імплементацію” (чи викликався setState, яке ім’я має клас, скільки <div/> всередині). Це була погана інженерна практика, оскільки будь-який рефакторинг змушував переписувати тести.
Творець RTL, Kent C. Dodds, ввів золоте правило тестування UI:
“Чим більше ваші тести наближені до того, як програму використовує кінцевий користувач, тим більше впевненості вони вам дають.”
Користувачу все одно, чи використовуєте ви useState, чи useReducer, або який CSS-клас у кнопки. Користувач шукає текст “Надіслати” і клікає по ньому. Саме так працює RTL — він віртуально (за допомогою JSDOM) рендерить компонент і дозволяє шукати елементи як це робить “сліпа людина” (через ARIA-ролі, тексти та плейсхолдери).
У RTL є три категорії функцій для пошуку елементів у відрендереному віртуальному DOM:
getBy... (Синхронний пошук): Повертає елемент АБО викидає помилку і “валить” тест, якщо елемента немає. (Найуживаніший).queryBy... (Синхронний пошук): Повертає елемент АБО повертає null. (Використовується виключно для перевірки того, що елемента НЕМАЄ на екрані).findBy... (Асинхронний пошук, Promise): Чекає кілька секунд, поки елемент не з’явиться (наприклад, після фетчингу даних з API). Або викидає помилку через таймаут.Пріоритет пошуку має спиратися на доступність (Accessibility / a11y): Найкраще шукати через getByRole, гірше — через ідентифікатори getByTestId (спеціальні атрибути data-testid).
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom"; // Розширює expect() методами типу toBeInTheDocument()
import Greeting from "./Greeting";
describe("<Greeting />", () => {
it("відмальовує привітання для користувача", () => {
// 1. Рендеримо компонент у віртуальний JSDOM
render(<Greeting name="Іван" />);
// 2. Шукаємо елемент заголовка h1, h2, h3...
// Користувач бачить текст, тому ми перевіряємо як користувач
const heading = screen.getByRole("heading", { level: 1 });
// 3. Асерція
expect(heading).toHaveTextContent("Привіт, Іван!");
expect(heading).toBeInTheDocument();
});
});
fireEventЩоб перевірити інтерактивність (кліки, ввід тексту), ми повинні імітувати фізичні дії.
У RTL є вбудована утиліта fireEvent, але вона відправляє лише одну синтетичну подію браузеру.
Наприклад, fireEvent.change(input, { target: { value: 'a' } }) спрацює, але в реальному браузері, коли користувач натискає на клавіатуру, насправді генеруються події: keyDown, keyPress, зміна тексту, input, keyUp.
Для справжньої (Enterprise) імітації використовують бібліотеку @testing-library/user-event.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import LoginForm from "./LoginForm";
it("повинен зробити кнопку активною після вводу пароля", async () => {
render(<LoginForm />);
// Ініціалізуємо імітатор користувача
const user = userEvent.setup();
const passwordInput = screen.getByLabelText("Пароль");
const submitButton = screen.getByRole("button", { name: "Увійти" });
// Перевіряємо початковий стан
expect(submitButton).toBeDisabled();
// Симулюємо РЕАЛЬНИЙ ввід тексту на клавіатурі (з усіма keyDown подіями)
await user.type(passwordInput, "admin123");
// Перевіряємо, чи компонент відреагував правильно
expect(submitButton).toBeEnabled();
// Симулюємо клік
await user.click(submitButton);
});
Головне правило Unit/Integration тестів: Тести ніколи не повинні ходити в інтернет до справжнього бази даних або API. Вони мають бути повністю ізольованими, детермінованими і швидкими.
Якщо ваш компонент робить axios.get('/users'), ви зобов’язані зробити Mock (Заглушку) цієї функції — підмінити її на вашу “фейкову” функцію, яка миттєво повертає наперед визначені (захардкоджені) дані. Це робиться за допомогою jest.mock.
import { render, screen } from "@testing-library/react";
import axios from "axios";
import UserList from "./UserList";
// Перехоплюємо весь модуль axios
jest.mock("axios");
it("повинен вивести список користувачів з API", async () => {
// Налаштовуємо нашу заглушку: "Коли хтось викличе get, поверни цей Promise(resolve)"
const mockData = { data: [{ id: 1, name: "Alice" }] };
axios.get.mockResolvedValueOnce(mockData);
render(<UserList />);
// Оскільки запит "асинхронний", елементи не з'являться миттєво.
// Використовуємо .findBy... (чекаємо до появи в DOM)
const listItem = await screen.findByText("Alice");
expect(listItem).toBeInTheDocument();
// Додаткова архітектурна асерція: Перевіряємо, чи axios був викликаний правильним роутом!
expect(axios.get).toHaveBeenCalledWith("/users");
expect(axios.get).toHaveBeenCalledTimes(1);
});
В сучасній розробці також популярним є підхід MSW (Mock Service Worker) — інстумент, який створює віртуальний сервер прямо в браузерері/Node.js і перехоплює HTTP-запити на рівні мережевого стеку, а не шляхом ламання модулів через jest.mock.
Після того, як сотні Unit-тестів довели міцність окремих “цеглин”, архітекторам потрібно переконатися, що весь “будинок” не розвалиться.
E2E-тестування (End-to-End) — це написання скриптів, які керують справжнім браузером (через веб-драйвери). В екосистемі наразі лідирує Playwright (від Microsoft) та класичний Cypress.
В E2E тесті ми буквально відкриваємо реальний URL додатку, який вже “крутиться” на тестовому сервері.
// Приклад тесту на Playwright (file.spec.js)
import { test, expect } from "@playwright/test";
test("головний потік покупки товару", async ({ page }) => {
// 1. Заходимо на розгорнутий тестовий сервер
await page.goto("http://localhost:3000");
// 2. Сценарій клієнта
await page.click("text=Каталог");
await page.click('button:has-text("Додати до кошика")');
await page.click('a[aria-label="Кошик"]');
// 3. Асерція
await expect(page.locator(".cart-total")).toHaveText("1000 грн");
});
Інтеграція в CI/CD (Continuous Integration): E2E тести і Jest тести вбудовуються у пайплайни розробки (напр., GitHub Actions). Створюється правило репозиторію: розробник пише код -> створює Pull Request -> Спрацьовує CI сервер, який в хмарі запускає всі 1000 тестів. Поки статус (Check) не стане зеленим (PASSED), кнопка “Merge into Main Branch” заблокована. Це і є запорукою інженерної стабільності.
user: null є хрупким (Brittle) тестом, що ламається при рефакторингу. Перевірка наявності кнопки “Login” за текстом або ARIA-роллю — надійний тест.user-event дозволяє асинхронно відтворювати всю послідовність нативних DOM-подій (event bubbling), які супроводжують натискання клавіш та миші, на відміну від обрізаного fireEvent.expect(wrapper.state('count')).toBe(1) (тестування внутрішнього стейту) є серйозним антипатерном і порушенням філософії надійності?fireEvent.change() та симуляцією через пакет @testing-library/user-event? Наведіть приклад, коли використання лише першого варіанту “ховає” баг від тестувальника.axios, як саме ми повинні поводитися з цим викликом при написанні Unit-тесту компонента?queryByRole замість getByRole? (Порада: розгляньте сценарій умовного рендерингу відсутності елемента).