Комментарии (2)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как покрывать код тестами на Frontend
Это очень частый вопрос на собеседованиях. Интервьюер хочет понять:
- Знаешь ли ты разницу между unit/integration/e2e тестами
- Понимаешь ли значение тестирования
- Как ты подходишь к тестированию в React
1. Стратегия покрытия (в порядке приоритета)
У меня есть пирамида тестирования:
E2E (10-20%)
- Важные пользовательские сценарии
- Интеграция фронта и бэка
- Используем Playwright
Integration (30-40%)
- Взаимодействие компонентов
- API моки (VCR.py, MSW)
- Testing Library queries
Unit (50-70%)
- Функции, утилиты
- Простые компоненты
- Виест (Vitest)
Все вместе дают 80-90% coverage.
2. Unit тесты в React (Testing Library)
// Компонент
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p data-testid="count">Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
// Тест (TDD подход)
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
describe('Counter', () => {
// Требование 1: показывает начальное значение
test('показывает начальное значение 0', () => {
render(<Counter />);
expect(screen.getByTestId('count')).toHaveTextContent('Count: 0');
});
// Требование 2: увеличивает при клике
test('увеличивает счетчик при клике на кнопку', async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole('button', { name: '+' });
await user.click(button);
expect(screen.getByTestId('count')).toHaveTextContent('Count: 1');
});
// Требование 3: увеличивает несколько раз
test('увеличивает несколько раз подряд', async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole('button', { name: '+' });
await user.click(button);
await user.click(button);
await user.click(button);
expect(screen.getByTestId('count')).toHaveTextContent('Count: 3');
});
});
3. Тесты форм
// Компонент формы
export function LoginForm({ onSubmit }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (!email || !password) {
setError('Оба поля обязательны');
return;
}
setLoading(true);
try {
await onSubmit({ email, password });
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
data-testid="email-input"
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
data-testid="password-input"
/>
{error && <div data-testid="error-message">{error}</div>}
<button type="submit" disabled={loading}>
{loading ? 'Загрузка...' : 'Войти'}
</button>
</form>
);
}
// Тесты
describe('LoginForm', () => {
// Тест валидации
test('показывает ошибку если поля пусты', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
const submitButton = screen.getByRole('button', { name: /войти/i });
await user.click(submitButton);
expect(screen.getByTestId('error-message')).toHaveTextContent(
'Оба поля обязательны'
);
});
// Тест успешной отправки
test('отправляет форму с корректными данными', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByTestId('email-input'), 'test@example.com');
await user.type(screen.getByTestId('password-input'), 'password123');
await user.click(screen.getByRole('button', { name: /войти/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
// Тест обработки ошибок
test('показывает ошибку от сервера', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
onSubmit.mockRejectedValue(new Error('Invalid credentials'));
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByTestId('email-input'), 'test@example.com');
await user.type(screen.getByTestId('password-input'), 'wrong');
await user.click(screen.getByRole('button', { name: /войти/i }));
expect(screen.getByTestId('error-message')).toHaveTextContent(
'Invalid credentials'
);
});
// Тест состояния загрузки
test('показывает состояние загрузки', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn(
() => new Promise(resolve => setTimeout(resolve, 100))
);
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByTestId('email-input'), 'test@example.com');
await user.type(screen.getByTestId('password-input'), 'password123');
const button = screen.getByRole('button');
await user.click(button);
expect(button).toHaveTextContent('Загрузка...');
expect(button).toBeDisabled();
});
});
4. Тесты с API моками (MSW)
import { setupServer } from 'msw/node';
import { HttpResponse, http } from 'msw';
// Моки API
const server = setupServer(
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({
id: params.id,
name: 'John Doe',
email: 'john@example.com',
});
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json();
return HttpResponse.json(
{ id: '1', ...body },
{ status: 201 }
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// Компонент с API
export function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
// Тест
test('загружает и показывает пользователя', async () => {
render(<UserProfile userId="123" />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(await screen.findByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
// Тест обработки ошибок
test('показывает ошибку при ошибке API', async () => {
server.use(
http.get('/api/users/:id', () => {
return HttpResponse.error();
})
);
render(<UserProfile userId="123" />);
expect(await screen.findByText(/error/i)).toBeInTheDocument();
});
5. E2E тесты (Playwright)
import { test, expect } from '@playwright/test';
test('полный сценарий логина', async ({ page }) => {
// 1. Переходим на страницу логина
await page.goto('http://localhost:3000/login');
// 2. Заполняем форму
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
// 3. Нажимаем кнопку
await page.click('button:has-text("Войти")');
// 4. Проверяем редирект на профиль
await expect(page).toHaveURL('/profile');
// 5. Проверяем что логин произошел
await expect(page.locator('h1')).toContainText('Профиль');
});
test('ошибка при неправильном пароле', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="password-input"]', 'wrongpassword');
await page.click('button:has-text("Войти")');
// Проверяем ошибку
await expect(
page.locator('[data-testid="error-message"]')
).toContainText('Invalid credentials');
// Остаемся на странице логина
await expect(page).toHaveURL('/login');
});
6. Как я подхожу к тестированию
// Мой процесс (TDD):
// ШАГ 1: Напишу список требований
const requirements = [
"Показывает список пользователей",
"Может отфильтровать по имени",
"Может отсортировать",
"Ловит ошибки загрузки",
"Показывает скелетон при загрузке",
];
// ШАГ 2: Напишу падающие тесты
test.todo('Показывает список пользователей');
test.todo('Может отфильтровать по имени');
// и т.д.
// ШАГ 3: Напишу минимальный код
// ТОЛЬКО чтобы тесты прошли
// ШАГ 4: Рефакторю, не ломая тесты
// ШАГ 5: Проверяю coverage
// npm run test:coverage
// Нужно 80%+ минимум
7. Команды
# Unit тесты в watch mode
npm test
# Один раз
npm run test:run
# Coverage отчет
npm run test:coverage
# Только определенный файл
npm test Counter.test.tsx
# E2E
npm run playwright
8. Частые ошибки в тестировании
// Ошибка 1: Тестирование деталей реализации (BAD)
test('вызывает setCount с 1', () => {
const setCount = vi.fn();
render(<Counter setCount={setCount} />);
// Это ломается при рефакторе
});
// Хорошо: Тестируем поведение
test('увеличивает счетчик', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button'));
expect(screen.getByText('1')).toBeInTheDocument();
});
// Ошибка 2: Неправильные селекторы (BAD)
test('показывает название', () => {
render(<Card />);
expect(screen.getByClassName('card-title')).toBeInTheDocument();
// Селектор по классу ломается при изменении стилей
});
// Хорошо: Используй data-testid
test('показывает название', () => {
render(<Card />);
expect(screen.getByTestId('card-title')).toBeInTheDocument();
});
// Ошибка 3: Не ждешь async операций
test('загружает данные', () => {
render(<UserList />);
// Нельзя просто проверять, нужно ждать
expect(screen.getByText('John')).toBeInTheDocument(); // FAIL
});
// Хорошо: Используй waitFor или findBy
test('загружает данные', async () => {
render(<UserList />);
expect(await screen.findByText('John')).toBeInTheDocument();
});
Итого
У меня есть иерархия тестирования:
- Unit (70%) - функции, компоненты по отдельности
- Integration (20%) - взаимодействие компонентов и API
- E2E (10%) - полные пользовательские сценарии
Использую TDD подход: требования -> падающие тесты -> код. Всегда минимум 80% coverage. Использую Testing Library (не Enzyme). Не тестирую деталей реализации, только поведение.