← Назад к вопросам

Как тестируешь компоненты?

2.0 Middle🔥 132 комментариев
#JavaScript Core

Комментарии (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 - тестирование хуков

Ключ к хорошему тестированию - тестировать поведение, а не реализацию. Тесты должны выжить рефакторинг, если функциональность не изменилась.

Как тестируешь компоненты? | PrepBro