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

Как вызвать API в Unit тестах?

2.2 Middle🔥 191 комментариев
#Тестирование

Комментарии (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 мокирован
  • Данные предсказуемы
  • Работают офлайн