Комментарии (2)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Тестирование React компонентов: полный подход
Я использую многоуровневую стратегию тестирования: unit тесты, интеграционные тесты и E2E тесты. Каждый уровень проверяет разные аспекты приложения.
Уровни тестирования
1. Unit тесты компонентов
Unit тесты проверяют отдельные компоненты в изоляции. Я использую Vitest + React Testing Library:
// Button.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from './Button';
describe('Button Component', () => {
it('должна рендерить текст кнопки', () => {
render(<Button>Нажми меня</Button>);
expect(screen.getByRole('button', { name: /нажми меня/i })).toBeInTheDocument();
});
it('должна вызвать onClick при клике', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(<Button onClick={handleClick}>Клик</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledOnce();
});
it('должна быть отключена когда disabled=true', () => {
render(<Button disabled>Отключено</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
it('должна применять правильный CSS класс для variant', () => {
const { rerender } = render(<Button variant="primary">Основной</Button>);
expect(screen.getByRole('button')).toHaveClass('variant-primary');
rerender(<Button variant="secondary">Дополнительный</Button>);
expect(screen.getByRole('button')).toHaveClass('variant-secondary');
});
});
2. Тестирование хуков
Тестирую кастомные хуки с помощью renderHook:
// useCounter.test.ts
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
describe('useCounter Hook', () => {
it('должен инициализировать счётчик с начальным значением', () => {
const { result } = renderHook(() => useCounter(5));
expect(result.current.count).toBe(5);
});
it('должен увеличивать счётчик при вызове increment', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('должен снижать счётчик при вызове decrement', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
it('должен сбросить счётчик при вызове reset', () => {
const { result } = renderHook(() => useCounter(10, 0));
result.current.increment();
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(0);
});
});
3. Тестирование с состоянием и эффектами
// UserProfile.test.tsx
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
describe('UserProfile Component with API calls', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('должна загружать и отображать данные пользователя', async () => {
const mockFetch = vi.spyOn(global, 'fetch').mockResolvedValueOnce({
ok: true,
json: async () => ({ id: 1, name: 'Иван', email: 'ivan@example.com' })
});
render(<UserProfile userId="1" />);
expect(screen.getByText(/загрузка/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Иван')).toBeInTheDocument();
expect(screen.getByText('ivan@example.com')).toBeInTheDocument();
});
expect(mockFetch).toHaveBeenCalledWith('/api/users/1');
});
it('должна отображать ошибку при неудачной загрузке', async () => {
vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'));
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText(/ошибка загрузки/i)).toBeInTheDocument();
});
});
});
4. Тестирование форм
// LoginForm.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
describe('LoginForm Component', () => {
it('должна валидировать email перед отправкой', async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
const submitButton = screen.getByRole('button', { name: /вход/i });
await user.click(submitButton);
expect(handleSubmit).not.toHaveBeenCalled();
expect(screen.getByText(/неверный email/i)).toBeInTheDocument();
});
it('должна отправлять форму с корректными данными', async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'user@example.com');
await user.type(screen.getByLabelText(/пароль/i), 'password123');
await user.click(screen.getByRole('button', { name: /вход/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123'
});
});
});
5. E2E тесты с Playwright
// auth.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Authentication Flow', () => {
test('должна позволить пользователю зарегистрироваться', async ({ page }) => {
await page.goto('http://localhost:3000/register');
await page.fill('input[name="email"]', 'newuser@example.com');
await page.fill('input[name="password"]', 'SecurePass123!');
await page.fill('input[name="confirmPassword"]', 'SecurePass123!');
await page.click('button:has-text("Зарегистрироваться")');
await expect(page).toHaveURL(/.*\/dashboard/);
await expect(page.locator('text=Добро пожаловать')).toBeVisible();
});
test('должна предотвратить логин с неверными кредентшалами', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.fill('input[name="email"]', 'wrong@example.com');
await page.fill('input[name="password"]', 'wrongpassword');
await page.click('button:has-text("Вход")');
await expect(page.locator('text=Неверные кредентшалы')).toBeVisible();
});
});
Организация тестов
Я следую структуре Arrange-Act-Assert:
it('должна обновлять счётчик', async () => {
// ARRANGE - подготовка данных и рендеринг
const { getByRole } = render(<Counter initialValue={0} />);
const button = getByRole('button');
// ACT - выполнение действия
await userEvent.click(button);
// ASSERT - проверка результата
expect(button).toHaveTextContent('1');
});
Покрытие тестами
Я стремлюсь к покрытию 80-90%:
- Критичные части (auth, оплата): 100%
- Компоненты UI: 80%
- Утилиты: 95%
- Игнорирую console errors, форматирование, визуальные вещи
Инструменты, которые использую
- Vitest - быстрые unit тесты
- React Testing Library - тестирование с точки зрения пользователя
- userEvent - реалистичная имитация действий
- Playwright - E2E тесты
- jest-mock-extended - моки и спаи
- @testing-library/react-hooks - тестирование хуков
Ключ к хорошему тестированию - тестировать поведение, а не реализацию. Тесты должны выжить рефакторинг, если функциональность не изменилась.