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

Для покрытия проекта выберешь End-to-End или Unit тесты

2.3 Middle🔥 251 комментариев
#Инструменты и DevOps#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Покрытие проекта: E2E vs Unit тесты

Это классический вопрос в тестировании. Правильный ответ не "выбери один", а "используй оба в разной пропорции в зависимости от контекста".

Пирамида тестирования (The Testing Pyramid)

        /\       <- E2E (5-10%)
       /  \      Медленные, дорогие, но тестируют реальный поток
      /____\
     /      \    <- Integration (15-30%)
    /        \   Тестируют взаимодействие компонентов
   /__________\
  /            \ <- Unit (60-70%)
 /              \ Быстрые, дешевые, тестируют изолированный код
/______________\

Это не просто теория, а proven best practice в индустрии (статья Майка Чина, Google Testing Blog).

Unit тесты (70-80% покрытия)

Unit тест — проверяет одну функцию или компонент в изоляции.

// utils/grades.ts
export function getGradeColor(grade: number): string {
  if (grade >= 80) return 'green';
  if (grade >= 60) return 'yellow';
  return 'red';
}

// __tests__/grades.test.ts
import { getGradeColor } from '../utils/grades';

describe('getGradeColor', () => {
  it('returns green for grade >= 80', () => {
    expect(getGradeColor(80)).toBe('green');
    expect(getGradeColor(95)).toBe('green');
  });

  it('returns yellow for grade 60-79', () => {
    expect(getGradeColor(60)).toBe('yellow');
    expect(getGradeColor(79)).toBe('yellow');
  });

  it('returns red for grade < 60', () => {
    expect(getGradeColor(59)).toBe('red');
    expect(getGradeColor(0)).toBe('red');
  });
});

Характеристики:

  • Выполняется за миллисекунды
  • Не требует браузера или сервера
  • Легко найти баг — точное место в коде
  • Легко мокировать зависимости
  • Можно запустить 1000 тестов за 1-2 секунды

Что тестировать:

// Функции преобразования
function formatDate(date: Date): string { }
function calculateTotal(items: Item[]): number { }

// Логика компонентов (через props)
function QuestionCard({ solved, onClick }) { }

// Хуки
function useQuestions(categoryId: string) { }

// Utils, helpers, сервисы
function validateEmail(email: string): boolean { }

Пример unit теста компонента:

import { render, screen, fireEvent } from '@testing-library/react';
import { QuestionCard } from './QuestionCard';

describe('QuestionCard', () => {
  it('renders question title', () => {
    const question = { id: '1', title: 'What is React?' };
    render(<QuestionCard question={question} />);
    
    expect(screen.getByText('What is React?')).toBeInTheDocument();
  });

  it('calls onClick when answer button is clicked', () => {
    const handleClick = vi.fn();
    const question = { id: '1', title: 'Question' };
    render(<QuestionCard question={question} onClick={handleClick} />);
    
    fireEvent.click(screen.getByRole('button', { name: /answer/i }));
    
    expect(handleClick).toHaveBeenCalledWith(question.id);
  });

  it('shows solved state when solved=true', () => {
    render(<QuestionCard question={{...}} solved={true} />);
    
    expect(screen.getByTestId('check-icon')).toBeInTheDocument();
  });
});

Integration тесты (15-30% покрытия)

Integration тест — проверяет как несколько компонентов или модулей работают вместе.

// Integration: QuestionList использует QuestionCard
import { render, screen, waitFor } from '@testing-library/react';
import { QuestionList } from './QuestionList';
import * as questionService from '@/lib/services/questionService';

vi.mock('@/lib/services/questionService');

describe('QuestionList - Integration', () => {
  it('fetches and displays questions', async () => {
    const mockQuestions = [
      { id: '1', title: 'Q1' },
      { id: '2', title: 'Q2' }
    ];
    
    vi.spyOn(questionService, 'getAll').mockResolvedValue(mockQuestions);
    
    render(<QuestionList />);
    
    // Ждём загрузки
    expect(screen.getByText('Loading...')).toBeInTheDocument();
    
    // Ждём отображения вопросов
    await waitFor(() => {
      expect(screen.getByText('Q1')).toBeInTheDocument();
      expect(screen.getByText('Q2')).toBeInTheDocument();
    });
  });

  it('handles API error gracefully', async () => {
    vi.spyOn(questionService, 'getAll')
      .mockRejectedValue(new Error('API Error'));
    
    render(<QuestionList />);
    
    await waitFor(() => {
      expect(screen.getByText(/error/i)).toBeInTheDocument();
    });
  });
});

Что тестировать:

  • Взаимодействие родителя и детей
  • Контекст и провайдеры
  • Сложные flows с несколькими компонентами

End-to-End тесты (5-10% покрытия)

E2E тест — проверяет реальный пользовательский сценарий через браузер.

// e2e/interview.spec.ts (Playwright)
import { test, expect } from '@playwright/test';

test.describe('Interview Flow', () => {
  test('user can solve question from start to finish', async ({ page }) => {
    // 1. Авторизация
    await page.goto('http://localhost:3000/login');
    await page.fill('input[type="email"]', 'user@example.com');
    await page.fill('input[type="password"]', 'password123');
    await page.click('button:has-text("Login")');
    
    // 2. Ждём перенаправления на главную
    await expect(page).toHaveURL('http://localhost:3000/questions');
    
    // 3. Выбираем категорию
    await page.click('text=React');
    await page.waitForSelector('[data-testid="question-card"]');
    
    // 4. Открываем вопрос
    const firstQuestion = page.locator('[data-testid="question-card"]').first();
    await firstQuestion.click();
    
    // 5. Видим вопрос и варианты ответов
    await expect(page.locator('h1')).toContainText('What is React?');
    const answers = page.locator('[data-testid="answer-option"]');
    expect(await answers.count()).toBeGreaterThan(0);
    
    // 6. Выбираем ответ
    await answers.first().click();
    
    // 7. Отправляем
    await page.click('button:has-text("Submit")');
    
    // 8. Видим результат
    await expect(page.locator('[data-testid="result-status"]')).toContainText('Correct!');
    await expect(page.locator('[data-testid="check-icon"]')).toBeVisible();
  });

  test('responsive design on mobile', async ({ page }) => {
    await page.setViewportSize({ width: 375, height: 667 }); // iPhone SE
    await page.goto('http://localhost:3000/questions');
    
    // Проверяем что интерфейс адаптирован
    const card = page.locator('[data-testid="question-card"]').first();
    const box = await card.boundingBox();
    
    expect(box?.width).toBeLessThanOrEqual(375);
  });
});

Характеристики:

  • Выполняется долго (5-60 секунд за тест)
  • Требует реальный браузер и запущённый сервер
  • Медленнее, чем unit, но тестирует реальный поток
  • Сложнее написать и отладить
  • Нестабильнее (timing issues, DOM не обновился)

Сравнительная таблица

КритерийUnitIntegrationE2E
Скорость<1ms10-100ms1-10s
Масштабируемость1000+ в проекте100-30020-50
СтоимостьНизкаяСредняяВысокая
Сложность написанияЛегкоСреднеСложно
Поиск багаТочныйСреднийНечёткий ("что-то сломалось")
ТестируетФункцию изолированноНесколько компонентовВесь фунционал
Mock'ingМногоНормальноМинимум

Рекомендуемое распределение

Для типичного фронтенд-приложения:

70% Unit тесты:
- Utils функции
- Компоненты (props)
- Хуки
- Бизнес-логика

20% Integration тесты:
- Сложные flows
- Взаимодействие нескольких компонентов
- Работа с Context API

10% E2E тесты:
- Критические пути (login, buy, submit)
- Ключевые features
- Регрессионные тесты

Правильная стратегия (как я бы выбрал)

Начинаю с Unit:

// 1. Пишу простой unit тест
test('formatPrice returns correct format', () => {
  expect(formatPrice(1000)).toBe('$1,000.00');
});

// 2. Реализую функцию
export function formatPrice(price: number): string {
  return `$${price.toLocaleString('en-US', {
    minimumFractionDigits: 2
  })}`;
}

// 3. Тест проходит

Потом Integration для сложных компонентов:

test('QuestionCard shows loading then content', async () => {
  const { getByText, queryByText } = render(
    <QuestionCard id="1" />
  );
  
  // Loading state
  expect(getByText('Loading...')).toBeInTheDocument();
  
  // После загрузки
  await waitFor(() => {
    expect(queryByText('Loading...')).not.toBeInTheDocument();
    expect(getByText('Question Title')).toBeInTheDocument();
  });
});

И только E2E для критических flows:

test('user completes interview', async ({ page }) => {
  // Весь interview process от начала до конца
});

Антипаттерны (чего ИЗБЕГАТЬ)

// Плохо: Unit тест, который тестирует всё
test('QuestionCard', () => {
  render(<QuestionCard />);
  fireEvent.click(screen.getByRole('button'));
  // Проверяем что произошло 10 разных вещей
  // Слишком большой тест, сложно найти баг
});

// Плохо: E2E тесты вместо Unit
test('formatPrice', async ({ page }) => {
  await page.goto('http://localhost/price-formatter');
  // Запускаешь браузер чтобы протестировать функцию
  // Медленно и ненадёжно
});

// Плохо: Много E2E, мало Unit
// Если у тебя только E2E:
// - Тесты медленные
// - Сложно найти что сломалось
// - Дорого поддерживать

Практический совет для PrepBro

Что покрыть Unit'ами:

  • Функции проверки ответов
  • Расчёты скора и статистики
  • Форматирование данных
  • Валидация вводов
  • Фильтрация и сортировка вопросов

Что покрыть Integration'ом:

  • Список вопросов + QuestionCard
  • Фильтры + список результатов
  • Авторизация + redirect на главную

Что покрыть E2E'ом:

  • Полный flow: login -> выбор профессии -> ответ на вопрос -> результат
  • Сохранение прогресса
  • Критические ошибки

Выводы

  1. Не выбирай один — используй оба
  2. Пирамида важна — много Unit, меньше Integration, совсем мало E2E
  3. Unit быстрые — пиши их для всё
  4. E2E медленные — пиши только для критических flows
  5. Integration в середине — используй когда нужно тестировать взаимодействие
  6. Баланс = скорость + надёжность — быстрые tests (unit) + надёжность (E2E критических путей)