Пошук уроків, статей та іншого контенту
Розберете цілі тестування React-застосунків, піраміду тестів і користувацький підхід до перевірки поведінки.
Тестування — це перевірка того, що застосунок поводиться очікувано за певних умов.
У React-застосунках тестами можна перевірити:
чи відображається потрібний текст;
чи реагує інтерфейс на натискання;
чи можна заповнити та надіслати форму;
чи показується повідомлення про помилку;
чи правильно відображаються дані;
чи не зламалася вже реалізована поведінка після змін у коді.
Тест не замінює ручну перевірку і не гарантує відсутність усіх помилок. Його завдання — швидко та повторювано перевіряти важливі сценарії.
Наприклад, якщо кнопка реєстрації перестала працювати, автоматичний тест може повідомити про це одразу після зміни коду, ще до того, як проблему побачить користувач.
Регресія — це ситуація, коли нова зміна ламає функціональність, яка раніше працювала.
Припустімо, компонент має кнопку:
<button onClick={handleSubmit}>Зберегти</button>Пізніше розробник змінює логіку компонента, і кнопка більше не викликає потрібну функцію. Якщо для цього сценарію є тест, він одразу завершиться помилкою.
Тести допомагають змінювати код із більшою впевненістю. Після рефакторингу можна перевірити, що поведінка для користувача залишилася такою самою.
Це особливо важливо у великих застосунках, де одна зміна може впливати на багато компонентів.
Добрий тест показує, як має працювати компонент.
Наприклад, із такого тесту зрозуміло, що:
користувач вводить ім’я;
натискає кнопку;
бачить привітання.
Тест у такому разі є прикладом використання компонента, а не лише технічною перевіркою його внутрішнього коду.
Ручна перевірка потребує часу:
відкрити сторінку;
знайти потрібну форму;
ввести дані;
натиснути кнопку;
перевірити результат.
Автоматизований тест виконує ці кроки за секунди та може запускатися після кожної зміни.
Найважливіше — перевіряти поведінку, яка має значення для користувача або бізнесу.
До таких сценаріїв належать:
відображення основного вмісту;
успішне надсилання форми;
валідація неправильних даних;
перемикання стану кнопки;
показ індикатора завантаження;
відображення повідомлення про помилку;
перехід між важливими станами інтерфейсу.
Не потрібно створювати тест для кожного рядка коду. Тестування має зменшувати ризики, а не перетворюватися на перевірку деталей реалізації.
Під час тестування варто думати не про те, як компонент написаний, а про те, що робить користувач.
Порівняймо два варіанти перевірки.
expect(component.state.isSubmitted).toBe(true);Такий тест перевіряє внутрішній стан компонента. Якщо реалізацію переписати, але поведінка для користувача залишиться правильною, тест може зламатися без реальної проблеми.
expect(screen.getByText('Форму надіслано')).toBeInTheDocument();Цей варіант перевіряє видимий результат. Йому не важливо, чи використовується useState, інший хук або зовсім інша внутрішня реалізація.
Основне правило:
Тестуйте інтерфейс так, як ним користується людина.
Тому під час тесту краще:
знаходити кнопку за її текстом;
знаходити поле за підписом;
вводити дані в поле;
натискати кнопку;
перевіряти повідомлення на екрані.
Натомість бажано не залежати від:
назв внутрішніх змінних;
стану React-компонента;
структури внутрішніх функцій;
конкретної кількості елементів, якщо це не важливо для користувача;
CSS-класів, якщо вони не є частиною поведінки.
Розглянемо компонент форми. Користувач вводить ім’я та натискає кнопку. Після цього компонент показує привітання.
import { useState } from 'react';
export function GreetingForm() {
const [name, setName] = useState('');
const [greeting, setGreeting] = useState('');
function handleSubmit(event) {
event.preventDefault();
if (name.trim() !== '') {
setGreeting(`Привіт, ${name}!`);
}
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">Ваше ім’я</label>
<input
id="name"
value={name}
onChange={(event) => setName(event.target.value)}
/>
<button type="submit">Привітатися</button>
{greeting && <p>{greeting}</p>}
</form>
);
}import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, test } from 'vitest';
import { GreetingForm } from './GreetingForm';
test('показує привітання після надсилання імені', async () => {
const user = userEvent.setup();
render(<GreetingForm />);
const nameInput = screen.getByLabelText('Ваше ім’я');
const submitButton = screen.getByRole('button', {
name: 'Привітатися',
});
await user.type(nameInput, 'Олена');
await user.click(submitButton);
expect(screen.getByText('Привіт, Олена!')).toBeInTheDocument();
});Цей тест повторює дії користувача:
відображає форму;
знаходить поле за його підписом;
знаходить кнопку за її роллю та назвою;
вводить ім’я;
натискає кнопку;
перевіряє повідомлення, яке бачить користувач.
Тест не перевіряє, чи існує змінна greeting, і не залежить від того, як саме компонент зберігає стан.
Піраміда тестів описує співвідношення різних видів тестів у проєкті.
Знизу вгору вона зазвичай має такі рівні:
модульні тести;
інтеграційні тести;
наскрізні тести.
Модульний тест перевіряє невелику частину коду окремо від інших частин системи.
У React це може бути:
функція форматування даних;
функція валідації;
невеликий компонент;
обчислення, яке використовується в компоненті.
Такі тести зазвичай:
швидко виконуються;
легко локалізують помилку;
можуть перевіряти багато окремих випадків.
Наприклад, можна перевірити, що функція валідації повертає помилку для порожнього імені.
Інтеграційний тест перевіряє взаємодію кількох частин застосунку.
Наприклад:
форма взаємодіє з компонентом повідомлень;
компонент викликає функцію надсилання;
після відповіді змінюється стан інтерфейсу;
кілька компонентів разом відображають результат.
Тест форми з попереднього прикладу можна вважати перевіркою взаємодії між полем, кнопкою, станом форми та повідомленням.
Інтеграційні тести часто добре відповідають реальним сценаріям користувача, тому вони особливо корисні для React-застосунків.
Наскрізний, або end-to-end, тест перевіряє застосунок повністю — приблизно так, як його використовує людина в браузері.
Такий тест може:
відкрити сторінку;
увійти в обліковий запис;
відкрити форму;
заповнити її;
надіслати дані;
перевірити результат на іншій сторінці.
Наскрізні тести перевіряють багато частин системи одночасно: інтерфейс, маршрутизацію, сервер і роботу з даними.
Вони корисні для найважливіших сценаріїв, але зазвичай:
виконуються довше;
складніші в налаштуванні;
можуть бути чутливішими до зовнішніх проблем.
Зазвичай у проєкті має бути більше швидких тестів нижнього рівня та менше повільних тестів верхнього рівня.
Типовий розподіл виглядає так:
багато модульних тестів;
достатня кількість інтеграційних тестів;
невелика кількість наскрізних тестів для критичних сценаріїв.
Це не суворе правило, а орієнтир. Важливіше не точне співвідношення, а баланс між:
швидкістю запуску;
надійністю;
цінністю перевірок;
складністю підтримки.
Наприклад, перевірку форматування ціни недоцільно виконувати через браузерний end-to-end тест. Для неї достатньо невеликого модульного тесту.
Водночас сценарій «користувач може оформити замовлення» може потребувати інтеграційного або наскрізного тесту.
Хороший тест має бути:
зрозумілим;
зосередженим на одному сценарії;
незалежним від інших тестів;
стабільним;
близьким до дій користувача;
корисним у разі помилки.
Назва тесту має описувати очікувану поведінку:
test('показує помилку, якщо поле порожнє', () => {
// ...
});Краще за назву, яка описує внутрішню реалізацію:
test('викликає setError у handleSubmit', () => {
// ...
});Перша назва відповідає на питання: «Що має побачити або отримати користувач?». Друга розповідає лише про внутрішню будову компонента.
Перед написанням тесту поставте собі запитання:
Який важливий сценарій може зламатися?
Яку дію виконує користувач?
Який результат він має побачити?
Що станеться для неправильних даних?
Чи перевіряю я поведінку, а не випадкову деталь реалізації?
Наприклад, для форми входу важливими сценаріями можуть бути:
користувач бачить поля для електронної пошти та пароля;
правильні дані дають змогу надіслати форму;
порожнє поле показує повідомлення про помилку;
під час надсилання кнопка має відповідний стан;
помилка сервера відображається користувачу.
Не обов’язково одразу тестувати всі можливі комбінації. Почніть із найважливіших і найризикованіших сценаріїв.
Якщо тест напряму перевіряє стан або внутрішні функції компонента, він може ламатися після безпечного рефакторингу.
Краще перевіряти видимі зміни та доступну користувачу поведінку.
Один тест, який перевіряє весь застосунок від входу до оформлення замовлення, складно зрозуміти та налагодити.
Краще розділяти різні сценарії на окремі тести.
Помилки є важливою частиною поведінки застосунку. Перевіряйте не лише правильні дані, а й:
порожні поля;
неправильний формат;
відмову сервера;
повторне натискання;
стан завантаження.
Тест заради самого тесту не приносить користі. Якщо перевірка не захищає важливу поведінку та складна в підтримці, її цінність може бути низькою.
Пошук елемента за випадковим CSS-класом або складною структурою DOM може зламатися після зміни стилів.
За можливості використовуйте доступні для користувача ознаки:
текст;
підпис поля;
роль елемента;
назву кнопки.
Тести допомагають знаходити помилки та захищають застосунок від регресій.
Тестувати потрібно насамперед важливу поведінку, а не кожен рядок коду.
Користувацький підхід означає перевірку дій і результатів, доступних користувачу.
Піраміда тестів містить модульні, інтеграційні та наскрізні тести.
Модульні тести зазвичай швидші, а наскрізні перевіряють ширші сценарії, але потребують більше часу.
Хороший тест описує очікувану поведінку та не залежить від зайвих деталей реалізації.
Для React-компонентів особливо цінні тести, які повторюють реальні дії користувача: пошук елемента, введення даних, натискання та перевірку результату.