Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как вызвать API в Unit тестах?
Это критически важный вопрос. Неправильно вызывать реальный API в unit тестах — это замедляет тесты, делает их хрупкими и зависящими от интернета. Нужно мокировать API.
Почему НЕ вызывать реальный API
// ❌ ПЛОХО - вызов реального API
test('fetches users', async () => {
const response = await fetch('https://api.example.com/users');
const users = await response.json();
expect(users.length).toBeGreaterThan(0);
});
// Проблемы:
// - Тест зависит от интернета и сервера
// - Если сервер downtime -> тест падает
// - Медленный тест (1-5 секунд)
// - Нельзя запустить офлайн
// - Нагружаем боевой сервер
// - Непредсказуемые данные (данные на сервере меняются)
Правильный подход: мокирование
1. Использование Jest Mock
Это самый простой способ для Node.js тестов:
// userService.js
export async function fetchUsers() {
const response = await fetch('https://api.example.com/users');
return response.json();
}
// userService.test.js
import * as userService from './userService';
// Мокируем встроенный fetch
global.fetch = jest.fn();
test('fetches users', async () => {
// Устанавливаем что вернёт fetch
fetch.mockResolvedValueOnce({
json: async () => [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
});
// Вызываем функцию
const users = await userService.fetchUsers();
// Проверяем результат
expect(users).toHaveLength(2);
expect(users[0].name).toBe('Alice');
// Проверяем что fetch был вызван правильно
expect(fetch).toHaveBeenCalledWith('https://api.example.com/users');
});
// Очистка после теста
afterEach(() => {
fetch.mockClear();
});
2. Mock в Vitest (современный подход)
// userService.test.ts
import { describe, it, expect, vi } from 'vitest';
import { fetchUsers } from './userService';
describe('fetchUsers', () => {
it('returns list of users', async () => {
// Мокируем fetch
global.fetch = vi.fn().mockResolvedValueOnce({
json: async () => [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
});
const users = await fetchUsers();
expect(users).toHaveLength(2);
expect(fetch).toHaveBeenCalledWith('https://api.example.com/users');
});
});
3. MSW (Mock Service Worker) — лучший способ
MSW перехватывает сетевые запросы на уровне браузера/Node, не изменяя код:
// mocks/handlers.js
import { http, HttpResponse } from 'msw';
export const handlers = [
// Мокируем GET /api/users
http.get('https://api.example.com/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]);
}),
// Мокируем POST /api/users
http.post('https://api.example.com/users', async ({ request }) => {
const newUser = await request.json();
return HttpResponse.json(
{ id: 3, ...newUser },
{ status: 201 }
);
}),
// Мокируем ошибку
http.get('https://api.example.com/error', () => {
return HttpResponse.json(
{ message: 'Something went wrong' },
{ status: 500 }
);
})
];
// mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// setup.js (конфиг Vitest/Jest)
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from './mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// Твой тест (без мокирования!)
test('fetches users', async () => {
// Код остаётся как был с реальным fetch
const response = await fetch('https://api.example.com/users');
const users = await response.json();
expect(users).toHaveLength(2);
// MSW автоматически перехватит запрос!
});
// Можно переопределить handler для конкретного теста
test('handles error', async () => {
server.use(
http.get('https://api.example.com/users', () => {
return HttpResponse.json(
{ error: 'Not found' },
{ status: 404 }
);
})
);
const response = await fetch('https://api.example.com/users');
expect(response.status).toBe(404);
});
Пример реальной компоненты и её теста
Компонента
// UserList.tsx
import { useEffect, useState } from 'react';
import { API_URL } from '@/config';
interface User {
id: number;
name: string;
email: string;
}
export function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchUsers();
}, []);
async function fetchUsers() {
try {
const response = await fetch(`${API_URL}/users`);
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
}
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
);
}
Тест с MSW
// UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, beforeEach } from 'vitest';
import { server } from '@/mocks/server';
import { http, HttpResponse } from 'msw';
import { UserList } from './UserList';
import { API_URL } from '@/config';
describe('UserList', () => {
it('displays users after loading', async () => {
// MSW автоматически вернёт users
render(<UserList />);
// Проверяем loading состояние
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Ждём пока данные загрузятся
await waitFor(() => {
expect(screen.getByText(/Alice/)).toBeInTheDocument();
});
// Проверяем что данные отображены
expect(screen.getByText('Alice (alice@example.com)')).toBeInTheDocument();
expect(screen.getByText('Bob (bob@example.com)')).toBeInTheDocument();
});
it('displays error when fetch fails', async () => {
// Переопределяем handler для этого теста
server.use(
http.get(`${API_URL}/users`, () => {
return HttpResponse.json(
{ error: 'Server error' },
{ status: 500 }
);
})
);
render(<UserList />);
await waitFor(() => {
expect(screen.getByText(/Error/)).toBeInTheDocument();
});
});
it('displays empty list', async () => {
server.use(
http.get(`${API_URL}/users`, () => {
return HttpResponse.json([]);
})
);
render(<UserList />);
await waitFor(() => {
expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
});
});
});
Для Axios (популярная библиотека)
// Если используешь axios
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
test('fetches users with axios', async () => {
const mock = new MockAdapter(axios);
mock.onGet('/users').reply(200, [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]);
const response = await axios.get('/users');
expect(response.data).toHaveLength(2);
});
VCR.py для интеграционных тестов
Если нужно записать реальный ответ один раз:
# Python backend тест
import vcr
with vcr.VCR().use_cassette('fixtures/users.yaml'):
response = requests.get('https://api.example.com/users')
assert response.status_code == 200
# Первый запуск: записывает реальный ответ в users.yaml
# Следующие запуски: использует записанный ответ
Типичные ошибки
// ❌ ПЛОХО - забыли мокировать
test('fetch users', async () => {
const users = await fetchUsers(); // Реальный запрос! Медленно!
expect(users.length).toBeGreaterThan(0);
});
// ❌ ПЛОХО - мок не перехватывает
test('fetch users', () => {
fetch.mockResolvedValueOnce({ json: async () => [] });
// Забыли async/await
expect(fetchUsers()).toBe([]);
});
// ✅ ХОРОШО
test('fetch users', async () => {
fetch.mockResolvedValueOnce({
json: async () => [{ id: 1, name: 'Alice' }]
});
const users = await fetchUsers();
expect(users).toHaveLength(1);
});
Итоговая рекомендация
Для unit тестов:
- Используй MSW (Mock Service Worker) — самый правильный способ
- Или Jest vi.fn() для простых моков
- Никогда не вызывай реальный API
Для интеграционных тестов:
- Можно использовать тестовый сервер
- Или VCR.py (запись/воспроизведение)
- Все равно не вызвай боевой API
Правило:
- Unit тесты = быстрые (< 100ms)
- API мокирован
- Данные предсказуемы
- Работают офлайн