Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как писал интеграционные тесты?
Определение интеграционного теста
Интеграционный тест проверяет взаимодействие между несколькими компонентами/слоями системы: React компоненты + кастомные хуки + API запросы. В отличие от unit тестов, которые изолируют отдельные функции, интеграционные тесты проверяют реальные сценарии пользователя.
Мой подход: React Testing Library + Mock API
// === Тестирование компонента с API запросом ===
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import UserProfile from '@/components/UserProfile';
// Мокируем API с MSW (Mock Service Worker)
const server = setupServer(
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({
id: params.id,
name: 'John Doe',
email: 'john@example.com',
});
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// Тест
it('displays user profile after loading', async () => {
render(<UserProfile userId="123" />);
// Проверяем, что идёт загрузка
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Ждём пока данные загрузятся
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
// === Тестирование взаимодействия компонент + хук ===
import { useForm } from '@/hooks/useForm';
function LoginForm() {
const { values, handleChange, handleSubmit, loading } = useForm({
email: '',
password: '',
});
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email}
onChange={handleChange}
placeholder="Email"
/>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
placeholder="Password"
/>
<button type="submit" disabled={loading}>
{loading ? 'Signing in...' : 'Sign in'}
</button>
</form>
);
}
it('submits login form with correct data', async () => {
const user = userEvent.setup();
server.use(
http.post('/api/login', async ({ request }) => {
const body = await request.json();
if (body.email === 'test@example.com' && body.password === 'pass123') {
return HttpResponse.json({ token: 'abc123' });
}
return HttpResponse.json({ error: 'Invalid credentials' }, { status: 401 });
})
);
render(<LoginForm />);
await user.type(screen.getByPlaceholderText('Email'), 'test@example.com');
await user.type(screen.getByPlaceholderText('Password'), 'pass123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(screen.getByText(/signed in/i)).toBeInTheDocument();
});
});
Интеграционные тесты с E2E (Playwright)
// === tests/e2e/user-profile.spec.ts ===
import { test, expect } from '@playwright/test';
test('User can view profile and edit information', async ({ page }) => {
// Авторизуемся через dev endpoint (если есть)
await page.post('/api/v1/dev/test-auth', {
data: {
userId: '123',
role: 'user',
},
});
// Переходим на страницу профиля
await page.goto('/profile');
// Проверяем, что данные загружены
await expect(page.getByText('John Doe')).toBeVisible();
// Кликаем кнопку редактирования
await page.getByRole('button', { name: /edit/i }).click();
// Редактируем поле
const nameInput = page.getByLabel('Full Name');
await nameInput.fill('Jane Doe');
// Отправляем форму
await page.getByRole('button', { name: /save/i }).click();
// Проверяем успешное сохранение
await expect(page.getByText('Changes saved')).toBeVisible();
await expect(page.getByText('Jane Doe')).toBeVisible();
});
test('Handles API errors gracefully', async ({ page }) => {
// Блокируем API запрос к /api/users
await page.route('/api/users/**', route => route.abort());
await page.goto('/profile');
// Проверяем сообщение об ошибке
await expect(page.getByText(/error loading profile/i)).toBeVisible();
});
Best Practices для интеграционных тестов
// OK: Тест отражает пользовательское поведение
it('user flow: search, filter, and view results', async () => {
render(<SearchPage />);
const searchInput = screen.getByPlaceholderText('Search');
await userEvent.type(searchInput, 'javascript');
const filterBtn = screen.getByRole('button', { name: /filter/i });
await userEvent.click(filterBtn);
await waitFor(() => {
expect(screen.getByText(/results for javascript/i)).toBeInTheDocument();
});
});
// NOT OK: Тест зависит от деталей реализации
it('sets state variable to loaded', () => {
const { result } = renderHook(() => useUserData());
expect(result.current.isLoaded).toBe(true);
});
// OK: Проверяем видимый результат, не состояние
it('displays user data after loading', async () => {
render(<UserComponent />);
await waitFor(() => {
expect(screen.getByText('User Name')).toBeInTheDocument();
});
});
// OK: Используем semantic queries
screen.getByRole('button', { name: /submit/i });
screen.getByLabelText('Email');
screen.getByText(/welcome/i);
// NOT OK: CSS селекторы хрупкие
screen.getByTestId('user-input');
container.querySelector('.form-input');
Структура папок для тестов
frontend/
|-- src/
| |-- components/
| | `-- UserProfile.tsx
| `-- hooks/
| `-- useUser.ts
|-- tests/
| |-- unit/
| | `-- hooks/
| | `-- useUser.test.ts
| |-- integration/
| | `-- UserProfile.test.tsx
| `-- e2e/
| `-- user-flow.spec.ts
Запуск тестов
# Unit + Integration тесты (Vitest)
npm test
# E2E тесты (Playwright)
npm run test:e2e
# Coverage
npm run test:coverage
# Watch mode
npm test -- --watch
Советы по написанию
- Один сценарий на тест — если нужно проверить несколько шагов, это один большой тест
- Реалистичные данные — используйте test factory или реальные примеры
- Тестируйте edge cases — пустые результаты, ошибки, таймауты
- Не мокируйте всё подряд — мокируйте только внешние API, остальное оставляйте реальным
- Проверяйте доступность —
getByRoleвместоgetByTestId