Для покрытия проекта выберешь End-to-End или Unit тесты
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Покрытие проекта: 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 не обновился)
Сравнительная таблица
| Критерий | Unit | Integration | E2E |
|---|---|---|---|
| Скорость | <1ms | 10-100ms | 1-10s |
| Масштабируемость | 1000+ в проекте | 100-300 | 20-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 -> выбор профессии -> ответ на вопрос -> результат
- Сохранение прогресса
- Критические ошибки
Выводы
- Не выбирай один — используй оба
- Пирамида важна — много Unit, меньше Integration, совсем мало E2E
- Unit быстрые — пиши их для всё
- E2E медленные — пиши только для критических flows
- Integration в середине — используй когда нужно тестировать взаимодействие
- Баланс = скорость + надёжность — быстрые tests (unit) + надёжность (E2E критических путей)