Разработка 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%. Вы получите стабильный набор тестов, который экономит время команды и повышает уверенность в каждой сборке. Получите консультацию по настройке тестирования — свяжитесь с нами. Закажите оценку вашего проекта уже сегодня.







