Пошук уроків, статей та іншого контенту
Перевірите ізольовані React-компоненти, їхній початковий стан, пропси та відображення різних варіантів.
Ізольоване тестування React-компонента означає, що ми перевіряємо його окремо від усього застосунку:
передаємо компоненту пропси;
перевіряємо початковий стан;
перевіряємо відображення різних варіантів;
взаємодіємо з елементами через дії користувача;
не підключаємо маршрутизацію, API або реальне сховище даних, якщо вони не потрібні саме для цього тесту.
Для тестування компонентів часто використовують:
Vitest — тестовий рушій;
React Testing Library — рендеринг компонентів і пошук елементів так, як їх бачить користувач;
jest-dom — додаткові перевірки на кшталт toBeInTheDocument;
user-event — симуляція дій користувача.
Встановлення залежностей для проєкту на Vite:
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event @vitejs/plugin-reactРозглянемо картку тарифного плану. Вона:
отримує назву, ціну та статус через пропси;
має початковий стан expanded;
показує різний текст для різних статусів;
розгортає та ховає додаткові деталі після натискання кнопки.
src/components/PlanCard.jsximport { useState } from 'react'
const statusLabels = {
active: 'Активна',
paused: 'Призупинена',
cancelled: 'Скасована',
}
export function PlanCard({
name,
price,
status = 'active',
initiallyExpanded = false,
}) {
const [expanded, setExpanded] = useState(initiallyExpanded)
const statusLabel = statusLabels[status] ?? statusLabels.active
return (
<article>
<h2>{name}</h2>
<p>
Ціна: {price} грн/місяць
</p>
<p role="status">
Статус: {statusLabel}
</p>
<button
type="button"
aria-expanded={expanded}
onClick={() => setExpanded((currentValue) => !currentValue)}
>
{expanded ? 'Сховати деталі' : 'Показати деталі'}
</button>
{expanded && (
<p>
Наступне списання: 1 травня
</p>
)}
</article>
)
}Vitest потрібно запустити в середовищі jsdom, оскільки React-компоненти працюють із DOM.
vite.config.jsimport { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: './src/test/setup.js',
},
})src/test/setup.jsimport '@testing-library/jest-dom/vitest'У package.json можна додати команду:
{
"scripts": {
"test": "vitest"
}
}Після цього тести запускаються командою:
npm testReact Testing Library надає функцію render, яка монтує компонент у тестовий DOM:
import { render } from '@testing-library/react'
import { PlanCard } from './PlanCard'
render(
<PlanCard
name="Pro"
price={499}
status="active"
/>,
)Після рендерингу елементи можна шукати через screen.
Бажано використовувати запити, орієнтовані на користувача:
getByRole — елемент із певною доступною роллю;
getByText — текстовий вміст;
getByLabelText — елемент за підписом;
queryBy... — запит, який повертає null, якщо елемент не знайдено.
Наприклад:
screen.getByRole('heading', { name: 'Pro' })
screen.getByRole('button', { name: 'Показати деталі' })
screen.getByText('Ціна: 499 грн/місяць')Пропси впливають на те, що бачить користувач. Тому тест має перевіряти не внутрішню змінну компонента, а результат її відображення.
src/components/PlanCard.test.jsximport { describe, expect, it } from 'vitest'
import { render, screen } from '@testing-library/react'
import { PlanCard } from './PlanCard'
describe('PlanCard', () => {
it('відображає назву та ціну, отримані через пропси', () => {
render(
<PlanCard
name="Команда"
price={799}
/>,
)
expect(
screen.getByRole('heading', { name: 'Команда' }),
).toBeInTheDocument()
expect(
screen.getByText('Ціна: 799 грн/місяць'),
).toBeInTheDocument()
})
})Цей тест не перевіряє, чи існує змінна name всередині компонента. Він перевіряє важливий для користувача результат: правильна назва з’явилася на сторінці.
Компонент отримує проп initiallyExpanded. Він визначає, чи видно деталі одразу після першого рендерингу.
Для перевірки початкового стану потрібно:
відрендерити компонент;
перевірити початкове значення aria-expanded;
перевірити наявність або відсутність деталей.
it('за замовчуванням ховає деталі', () => {
render(
<PlanCard
name="Pro"
price={499}
/>,
)
const button = screen.getByRole('button', {
name: 'Показати деталі',
})
expect(button).toHaveAttribute('aria-expanded', 'false')
expect(
screen.queryByText('Наступне списання: 1 травня'),
).not.toBeInTheDocument()
})
it('може одразу показати деталі', () => {
render(
<PlanCard
name="Pro"
price={499}
initiallyExpanded
/>,
)
const button = screen.getByRole('button', {
name: 'Сховати деталі',
})
expect(button).toHaveAttribute('aria-expanded', 'true')
expect(
screen.getByText('Наступне списання: 1 травня'),
).toBeInTheDocument()
})queryByText використовується саме для перевірки відсутності елемента. Якщо застосувати getByText, тест завершиться помилкою одразу під час пошуку.
Для симуляції дій користувача використовується userEvent. На відміну від безпосередньої зміни властивостей DOM, він імітує реальнішу послідовність дій.
import userEvent from '@testing-library/user-event'
it('показує та ховає деталі після натискання кнопки', async () => {
const user = userEvent.setup()
render(
<PlanCard
name="Pro"
price={499}
/>,
)
const showButton = screen.getByRole('button', {
name: 'Показати деталі',
})
await user.click(showButton)
expect(
screen.getByText('Наступне списання: 1 травня'),
).toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'Сховати деталі' }),
).toHaveAttribute('aria-expanded', 'true')
await user.click(
screen.getByRole('button', { name: 'Сховати деталі' }),
)
expect(
screen.queryByText('Наступне списання: 1 травня'),
).not.toBeInTheDocument()
})Методи userEvent є асинхронними, тому тест позначений як async, а перед дією використовується await.
Якщо компонент має кілька варіантів одного пропа, кожен важливий варіант потрібно перевірити.
Для цього зручно використовувати it.each. Він запускає один тест із різними наборами даних.
it.each([
['active', 'Активна'],
['paused', 'Призупинена'],
['cancelled', 'Скасована'],
])(
'відображає правильний текст для статусу %s',
(status, expectedLabel) => {
render(
<PlanCard
name="Pro"
price={499}
status={status}
/>,
)
expect(screen.getByRole('status')).toHaveTextContent(
`Статус: ${expectedLabel}`,
)
},
)У результаті Vitest перевірить три випадки:
active відображається як Активна;
paused відображається як Призупинена;
cancelled відображається як Скасована.
Такий підхід зменшує дублювання тестового коду і чітко показує список підтримуваних варіантів.
Нижче наведено повний приклад тестів для компонента PlanCard.
src/components/PlanCard.test.jsximport { describe, expect, it } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { PlanCard } from './PlanCard'
describe('PlanCard', () => {
it('відображає назву та ціну, отримані через пропси', () => {
render(
<PlanCard
name="Команда"
price={799}
/>,
)
expect(
screen.getByRole('heading', { name: 'Команда' }),
).toBeInTheDocument()
expect(
screen.getByText('Ціна: 799 грн/місяць'),
).toBeInTheDocument()
})
it('за замовчуванням ховає деталі', () => {
render(
<PlanCard
name="Pro"
price={499}
/>,
)
const button = screen.getByRole('button', {
name: 'Показати деталі',
})
expect(button).toHaveAttribute('aria-expanded', 'false')
expect(
screen.queryByText('Наступне списання: 1 травня'),
).not.toBeInTheDocument()
})
it('може одразу показати деталі', () => {
render(
<PlanCard
name="Pro"
price={499}
initiallyExpanded
/>,
)
const button = screen.getByRole('button', {
name: 'Сховати деталі',
})
expect(button).toHaveAttribute('aria-expanded', 'true')
expect(
screen.getByText('Наступне списання: 1 травня'),
).toBeInTheDocument()
})
it('показує та ховає деталі після натискання кнопки', async () => {
const user = userEvent.setup()
render(
<PlanCard
name="Pro"
price={499}
/>,
)
await user.click(
screen.getByRole('button', {
name: 'Показати деталі',
}),
)
expect(
screen.getByText('Наступне списання: 1 травня'),
).toBeInTheDocument()
await user.click(
screen.getByRole('button', {
name: 'Сховати деталі',
}),
)
expect(
screen.queryByText('Наступне списання: 1 травня'),
).not.toBeInTheDocument()
})
it.each([
['active', 'Активна'],
['paused', 'Призупинена'],
['cancelled', 'Скасована'],
])(
'відображає правильний текст для статусу %s',
(status, expectedLabel) => {
render(
<PlanCard
name="Pro"
price={499}
status={status}
/>,
)
expect(screen.getByRole('status')).toHaveTextContent(
`Статус: ${expectedLabel}`,
)
},
)
})Запустити тести в режимі спостереження можна командою:
npm testДля одноразового запуску в CI:
npx vitest runДля компонента середньої складності корисно перевірити:
Основний рендеринг
компонент створюється без помилок;
обов’язкові дані відображаються.
Пропси
текст, числа та інші значення з пропсів показуються правильно;
значення за замовчуванням працюють очікувано.
Початковий стан
приховані елементи справді приховані;
активний або відкритий варіант показується одразу, якщо це передано через проп.
Варіанти
кожен підтримуваний статус або режим має правильне відображення.
Дії користувача
натискання кнопки змінює видимий результат;
доступні атрибути, наприклад aria-expanded, оновлюються.
Не варто намагатися отримати useState або перевіряти назви внутрішніх змінних. Такі тести залежать від реалізації компонента.
Натомість перевіряйте результат:
expect(
screen.getByText('Наступне списання: 1 травня'),
).toBeInTheDocument()Тест на кшталт:
container.querySelector('.plan-card__details')прив’язаний до структури CSS. Після перейменування класу поведінка компонента може залишитися правильною, але тест зламається.
Краще шукати елементи за роллю, текстом або доступним ім’ям.
getBy... для відсутнього елементаgetByText очікує, що елемент існує, і кидає помилку, якщо його немає. Для перевірки відсутності використовуйте queryByText:
expect(
screen.queryByText('Наступне списання: 1 травня'),
).not.toBeInTheDocument()await для userEventВиклики userEvent потрібно очікувати:
await user.click(button)Без await перевірка може виконатися до того, як компонент оновить стан.
Великий тест, який перевіряє пропси, початковий стан, усі статуси та взаємодію одночасно, складно підтримувати. Краще створити короткі тести з однією зрозумілою метою.
Ізольований компонент можна тестувати без запуску всього застосунку.
render монтує React-компонент у тестовий DOM.
screen.getByRole допомагає шукати елементи з позиції користувача.
Пропси перевіряють через видимий результат у DOM.
Початковий стан перевіряють одразу після render.
userEvent використовується для реалістичної симуляції взаємодій.
it.each зручно застосовувати для перевірки кількох варіантів пропса.
Тести мають перевіряти поведінку та доступний інтерфейс компонента, а не його внутрішню реалізацію.