Розробка Unit-тестів для компонентів (React Testing Library)
Ви написали React-компонент, він працює, але після рефакторингу падають старі тести? Знайома ситуація, коли тести перевіряють внутрішню реалізацію, а не поведінку. При використанні Enzyme кожен рефакторинг ламає тести, гальмуючи розробку. React Testing Library (RTL) вирішує цю проблему: тести фокусуються на тому, що бачить користувач, і не ламаються при зміні внутрішньої логіки. Перехід з Enzyme на RTL скорочує час підтримки тестів у 2–3 рази, а економія бюджету команди на регресіях сягає 40 %. Ми допоможемо налаштувати тестування під ключ: від конфігурації середовища до впровадження в CI. Наші інженери мають 10+ років досвіду та сертифікати по React і TypeScript. React Testing Library рекомендована спільнотою як стандарт. Зв’яжіться з нами, щоб почати впровадження.
Навіщо React Testing Library, а не Enzyme?
| Критерій | Enzyme | RTL |
|---|---|---|
| Акцент на користувачі | Ні | Так |
| Доступ до state і props | Повний | Ні |
| Стійкість до рефакторингу | Низька | Висока |
| Час виконання тестів | Повільніше | Швидше на 30–40% |
| Простота підтримки | Крихкі тести | Стабільні тести |
Enzyme дає доступ до state, props і внутрішніх методів — це зручно на короткій дистанції, але призводить до крихкості. RTL використовує query-функції, орієнтовані на ролі, текст і мітки, як це роблять користувачі. Тести на RTL не ламаються при рефакторингу, а регресії практично виключені.
Як налаштувати тестове середовище?
Встановіть Vitest, jsdom, RTL, user-event і MSW. Дотримуйтесь документації Vitest для налаштування. Конфігурація мінімальна:
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event vitest jsdom msw
У vitest.config.ts вкажіть jsdom, глобальні змінні та setup-файл. Нижче приклад з порогами покриття:
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
thresholds: { statements: 80, branches: 75, functions: 80, lines: 80 },
},
},
});
У setup.ts додайте імпорт @testing-library/jest-dom та налаштування MSW.
Які проблеми вирішує кастомний render?
Компоненти залежать від контексту: роутер, стор, query-клієнт. Щоб не обгортати кожен тест вручну, створіть кастомний render з MemoryRouter і QueryClientProvider:
import { render, RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { ReactNode } from 'react';
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
}
export function renderWithProviders(
ui: React.ReactElement,
options?: RenderOptions & { initialEntries?: string[] }
) {
const { initialEntries = ['/'], ...rest } = options ?? {};
const queryClient = createTestQueryClient();
function Wrapper({ children }: { children: ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={initialEntries}>{children}</MemoryRouter>
</QueryClientProvider>
);
}
return render(ui, { wrapper: Wrapper, ...rest });
}
Такий підхід скорочує дублювання коду на 40% і робить тести одноманітними. Без кастомного render кожен тест містив би 5–10 рядків обгорток — тепер це один рядок.
Як тестувати асинхронні операції?
Використовуйте findBy для очікування появи елемента та MSW для мока запитів. Приклад форми авторизації:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('відображає поля та кнопку', () => {
render(<LoginForm onSubmit={vi.fn()} />);
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /увійти/i })).toBeInTheDocument();
});
it('викликає onSubmit з даними', async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), '[email protected]');
await user.type(screen.getByLabelText(/пароль/i), 'password123');
await user.click(screen.getByRole('button', { name: /увійти/i }));
expect(handleSubmit).toHaveBeenCalledWith({ email: '[email protected]', password: 'password123' });
});
it('показує помилку при порожньому email', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.click(screen.getByRole('button', { name: /увійти/i }));
expect(screen.getByText(/введіть email/i)).toBeInTheDocument();
expect(onSubmit).not.toHaveBeenCalled();
});
});
Для асинхронного завантаження з MSW:
import { renderWithProviders } from '@/test/render';
import { screen } from '@testing-library/react';
import { server } from '@/test/server';
import { http, HttpResponse } from 'msw';
import { UserProfile } from './UserProfile';
it('завантажує дані користувача', async () => {
renderWithProviders(<UserProfile userId="42" />);
expect(await screen.findByText('Test User')).toBeInTheDocument();
});
it('показує помилку при недоступному API', async () => {
server.use(http.get('/api/users/:id', () => HttpResponse.json({ message: 'Server error' }, { status: 500 })));
renderWithProviders(<UserProfile userId="42" />);
expect(await screen.findByText(/не вдалося завантажити/i)).toBeInTheDocument();
});
Типові помилки при тестуванні React-компонентів
| Помилка | Наслідки | Рішення |
|---|---|---|
| Тестування внутрішньої реалізації | Ламаються при рефакторингу | Використовуйте RTL, тестуйте поведінку |
| Відсутність моків API | Тести залежать від мережі | Використовуйте MSW |
| Немає кастомного render | Дублювання обгорток | Створіть renderWithProviders |
Що тестувати в першу чергу?
Покривайте тестами: умовний рендеринг, обробники подій, валідацію форм, стани завантаження та помилок, інтеграцію з роутингом. Не варто тестувати CSS-класи, внутрішні методи або snapshot-тести без змін — вони ламаються при будь-якій косметичній зміні. Економія ресурсів команди сягає 50% за рахунок скорочення регресійних багів.
Процес роботи
- Аналіз — виділяємо ключові компоненти та сценарії (3–5 днів).
- Налаштування — конфігуруємо середовище, пишемо кастомний render і моки (1–2 дні).
- Написання тестів — покриваємо критичні шляхи, форми, async-операції (від 4 годин на компонент).
- Інтеграція — підключаємо coverage в CI, налаштовуємо пороги (1 день).
- Навчання — проводимо code-review та консультації для вашої команди.
Строки
Для проєкту зі стандартним набором компонентів (5–10 форм, список, модалки) базове покриття займає від 5 до 10 днів. Складні інтеграції з MSW або Apollo Client можуть додати ще 2–3 дні. Вартість розраховується індивідуально після оцінки обсягу коду.
Що входить в роботу
- Налаштування тестового середовища (Vitest, jsdom, MSW).
- Кастомний render з провайдерами (Router, QueryClient).
- Покриття тестами ключових компонентів (60–80% розгалужень).
- Інтеграція coverage в CI (GitHub Actions, GitLab).
- Документація по тестуванню для розробників.
Чому варто впровадити тестування з RTL?
Ми гарантуємо, що тести не зламаються при рефакторингу, покриття буде не нижче 80%, а CI буде падати при падінні тестів. Зниження витрат на регресійне тестування сягає 40%. Ви отримаєте стабільний набір тестів, який економить час команди та підвищує впевненість у кожній збірці. Отримайте консультацію з налаштування тестування — зв’яжіться з нами. Замовте оцінку вашого проєкту вже сьогодні.







