Реиспользование mock handlers между тестами

В процессе тестирования компонентов React часто возникает необходимость имитировать сетевые запросы или внешние зависимости с помощью моков. Одним из ключевых инструментов для этого является msw (Mock Service Worker) или собственные функции-заглушки. При этом важно организовать мок-обработчики так, чтобы их можно было повторно использовать между тестами, избегая дублирования кода и облегчая поддержку тестовой среды.


Структура mock handlers

Обычно мок-обработчики оформляются как отдельные функции или массивы объектов, которые описывают поведение API:

import { rest } from 'msw';

export const handlers = [
  rest.get('/api/users', (req, res, ctx) => {
    return res(ctx.status(200), ctx.json([{ id: 1, name: 'Alice' }]));
  }),

  rest.post('/api/login', (req, res, ctx) => {
    const { username } = req.body;
    return res(
      ctx.status(200),
      ctx.json({ token: `token-for-${username}` })
    );
  }),
];

Ключевой момент: такие handlers можно импортировать в любой тестовый файл, что позволяет централизованно управлять моками.


Подключение mock-сервера

Чтобы использовать обработчики во всех тестах, создают отдельный файл для настройки сервера:

import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);

Далее в файле конфигурации тестов (setupTests.js или аналогичном) подключают сервер:

import { server } from './testServer';
import '@testing-library/jest-dom';

// Запуск перед всеми тестами
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));

// Сброс обработчиков после каждого теста
afterEach(() => server.resetHandlers());

// Завершение работы сервера после всех тестов
afterAll(() => server.close());

Это обеспечивает чистое тестовое окружение: каждый тест получает свежие настройки моков без побочных эффектов от предыдущих вызовов.


Динамическое переопределение обработчиков

Иногда требуется изменить поведение мока для конкретного теста, например, имитировать ошибку сервера:

import { rest } from 'msw';
import { server } from './testServer';

test('отображает сообщение об ошибке при неудачном запросе', async () => {
  server.use(
    rest.get('/api/users', (req, res, ctx) => {
      return res(ctx.status(500));
    })
  );

  render(<UserList />);
  expect(await screen.findByText(/ошибка/i)).toBeInTheDocument();
});

Метод server.use() добавляет новые обработчики для текущего теста, при этом глобальные handlers остаются неизменными для других тестов. После теста afterEach вызовет server.resetHandlers(), возвращая исходное состояние.


Разделение глобальных и локальных handlers

Для удобства можно разделять глобальные handlers, которые всегда используются, и тестовые, специфичные для отдельных сценариев:

// handlers/globalHandlers.js
export const globalHandlers = [
  rest.get('/api/users', (req, res, ctx) => res(ctx.json([{ id: 1, name: 'Alice' }])))
];

// handlers/testSpecificHandlers.js
export const errorHandlers = [
  rest.get('/api/users', (req, res, ctx) => res(ctx.status(500)))
];

В тесте можно комбинировать их:

import { server } from './testServer';
import { errorHandlers } from './handlers/testSpecificHandlers';

beforeEach(() => {
  server.use(...errorHandlers);
});

Такой подход упрощает поддержку больших проектов, где разные модули могут требовать разные варианты отклика API.


Реиспользование mock функций без msw

Если проект не использует msw, мок-обработчики можно хранить как отдельные функции:

export const fetchUserMock = jest.fn(() =>
  Promise.resolve([{ id: 1, name: 'Alice' }])
);

И использовать в разных тестах:

import { fetchUserMock } from './mocks';

beforeEach(() => {
  fetchUserMock.mockClear();
});

test('рендерит список пользователей', async () => {
  render(<UserList fetchUsers={fetchUserMock} />);
  expect(await screen.findByText('Alice')).toBeInTheDocument();
});

Преимущество: одинаковые моки не нужно дублировать, можно централизованно управлять поведением и состоянием вызовов.


Практические рекомендации

  • Всегда централизовать обработчики: хранить их в отдельной директории mocks/handlers.
  • Разделять глобальные и тестовые: глобальные для стандартного сценария, тестовые — для особых случаев.
  • Сбрасывать состояние после каждого теста (resetHandlers или mockClear), чтобы тесты оставались изолированными.
  • Использовать описательные имена функций и переменных, например successHandlers, errorHandlers, fetchUserMock.
  • Не дублировать моки: один handler можно подключать в разных тестах, изменяя поведение через динамическое переопределение.

Такой подход к реиспользованию mock handlers обеспечивает масштабируемость тестов, предотвращает дублирование кода и упрощает управление различными сценариями API-запросов в проектах на React.