Использование MSW для моков

RTK Query тесно взаимодействует с концепцией тестирования сетевого слоя, поскольку вся модель работы строится вокруг предсказуемых запросов, кэширования и автоматической синхронизации состояния. В этом контексте Mock Service Worker (MSW) становится ключевым инструментом для изоляции логики запросов от реального бэкенда и построения воспроизводимых сценариев.

MSW перехватывает запросы на уровне сетевого слоя, не требуя подмены fetch или внедрения заглушек в код. Это позволяет RTK Query работать в «реальной» модели выполнения, при этом получая контролируемые ответы.

RTK Query формирует запросы через fetchBaseQuery или кастомный baseQuery. Далее запрос уходит в fetch. MSW внедряется между приложением и сетью, перехватывая HTTP-вызовы:

  • RTK Query вызывает endpoint
  • fetch отправляет запрос
  • MSW intercepts request
  • возвращается мок-ответ
  • RTK Query получает данные и кладёт их в кэш

Ключевой момент: RTK Query не знает, что ответ поддельный. Это делает тесты максимально близкими к реальности.

Установка и базовая настройка MSW

Установка:

npm install msw --save-dev

Для Node.js тестов используется setupServer, для браузера — setupWorker.

Структура обычно выглядит так:

src/
  api/
  mocks/
    handlers.js
    server.js
    browser.js

Определение обработчиков запросов

Handlers описывают, какие запросы перехватываются и какие ответы возвращаются.

import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/posts', () => {
    return HttpResponse.json([
      { id: 1, title: 'Первый пост' },
      { id: 2, title: 'Второй пост' }
    ]);
  }),

  http.get('/api/posts/:id', ({ params }) => {
    return HttpResponse.json({
      id: Number(params.id),
      title: 'Пост по id'
    });
  })
];

MSW позволяет использовать динамические параметры, что особенно важно для RTK Query endpoints с аргументами.

Подключение MSW в тестовой среде Node.js

Файл server.js:

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

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

В конфигурации тестов:

import { server } from './mocks/server';

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

resetHandlers особенно важен при тестировании RTK Query, так как кэш и повторные запросы могут зависеть от состояния моков.

Интеграция RTK Query API слоя

Пример API слоя:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const postsApi = createApi({
  reducerPath: 'postsApi',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api'
  }),
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => '/posts'
    }),
    getPostById: builder.query({
      query: (id) => `/posts/${id}`
    })
  })
});

MSW полностью перехватывает /api/posts и /api/posts/:id, обеспечивая изолированное тестирование без реального сервера.

Тестирование query-запросов

Пример теста с Jest/Vitest:

import { store } from '../store';
import { postsApi } from '../api/postsApi';
import { server } from '../mocks/server';

test('загрузка списка постов', async () => {
  const result = store.dispatch(
    postsApi.endpoints.getPosts.initiate()
  );

  const data = await result.unwrap();

  expect(data).toHaveLength(2);
  expect(data[0].title).toBe('Первый пост');
});

RTK Query возвращает unwrap(), что упрощает работу с результатом в тестах.

Мокирование состояний ошибок

MSW позволяет моделировать HTTP ошибки без изменения логики RTK Query.

import { http, HttpResponse } from 'msw';

server.use(
  http.get('/api/posts', () => {
    return new HttpResponse(null, { status: 500 });
  })
);

Тестирование ошибки:

test('ошибка загрузки постов', async () => {
  const result = store.dispatch(
    postsApi.endpoints.getPosts.initiate()
  );

  await expect(result.unwrap()).rejects.toThrow();
});

RTK Query автоматически устанавливает error состояние, включая status и data.

Эмуляция задержек и асинхронности

Реальные API редко отвечают мгновенно, поэтому задержки важны для проверки состояния isLoading.

import { delay, http, HttpResponse } from 'msw';

http.get('/api/posts', async () => {
  await delay(300);

  return HttpResponse.json([
    { id: 1, title: 'Пост с задержкой' }
  ]);
});

RTK Query в этот момент корректно переключает состояния:

  • isLoading
  • isFetching
  • isSuccess

Перекрытие обработчиков в отдельных тестах

MSW позволяет локально переопределять поведение API:

server.use(
  http.get('/api/posts', () => {
    return HttpResponse.json([]);
  })
);

Это полезно для проверки пустых состояний UI и fallback-логики RTK Query.

Проверка кэширования RTK Query

Одно из ключевых преимуществ RTK Query — кэширование запросов. MSW помогает проверить, что повторные запросы не вызывают лишних сетевых обращений.

test('кэширование запроса posts', async () => {
  const first = store.dispatch(
    postsApi.endpoints.getPosts.initiate()
  );

  await first.unwrap();

  const second = store.dispatch(
    postsApi.endpoints.getPosts.initiate()
  );

  const data = await second.unwrap();

  expect(data).toHaveLength(2);
});

MSW можно дополнительно логировать, чтобы убедиться, что запрос был выполнен один раз.

Тестирование мутаций (mutation endpoints)

MSW одинаково хорошо работает с POST, PUT, DELETE.

http.post('/api/posts', async ({ request }) => {
  const body = await request.json();

  return HttpResponse.json({
    id: 123,
    ...body
  });
});

RTK Query mutation:

addPost: builder.mutation({
  query: (post) => ({
    url: '/posts',
    method: 'POST',
    body: post
  })
})

Тест:

test('создание поста', async () => {
  const result = store.dispatch(
    postsApi.endpoints.addPost.initiate({
      title: 'Новый пост'
    })
  );

  const data = await result.unwrap();

  expect(data.id).toBeDefined();
  expect(data.title).toBe('Новый пост');
});

Работа с auth-заголовками

RTK Query часто использует prepareHeaders. MSW позволяет проверять их наличие.

http.get('/api/posts', ({ request }) => {
  const auth = request.headers.get('authorization');

  if (!auth) {
    return new HttpResponse(null, { status: 401 });
  }

  return HttpResponse.json([]);
});

Это позволяет тестировать сценарии авторизации без реального backend.

Очистка состояния и изоляция тестов

RTK Query кэширует данные между вызовами, поэтому важно комбинировать MSW с очисткой store:

afterEach(() => {
  server.resetHandlers();
  store.dispatch(postsApi.util.resetApiState());
});

Это предотвращает утечки состояния между тестами.

Использование MSW в браузере для интеграционных тестов

В браузерной среде:

import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);

Запуск:

worker.start();

Это позволяет тестировать RTK Query прямо в dev-среде без backend.

Сценарии сложного поведения API

MSW позволяет моделировать:

Условные ответы

http.get('/api/posts', ({ request }) => {
  const url = new URL(request.url);
  const page = url.searchParams.get('page');

  return HttpResponse.json({
    page,
    items: []
  });
});

Рандомные ошибки

http.get('/api/posts', () => {
  if (Math.random() > 0.5) {
    return new HttpResponse(null, { status: 500 });
  }

  return HttpResponse.json([]);
});

Комбинация разных endpoint-ов

MSW позволяет централизованно управлять всеми API слоями, что важно при тестировании RTK Query с несколькими api slices.

Связка MSW и middleware RTK Query

RTK Query middleware реагирует на:

  • fulfilled
  • pending
  • rejected

MSW позволяет искусственно инициировать каждый из этих сценариев через контролируемые ответы. Это делает возможным тестирование side effects, optimistic updates и invalidation логики.

Тестирование инвалидации кеша

addPost: builder.mutation({
  query: (post) => ({ url: '/posts', method: 'POST', body: post }),
  invalidatesTags: ['Posts']
});

MSW возвращает успешный POST, после чего RTK Query автоматически делает refetch getPosts. Это поведение можно наблюдать и проверять через мок-слой без реального сервера.

Масштабирование моков

При росте проекта handlers обычно разделяются:

mocks/
  handlers/
    posts.js
    auth.js
    comments.js

И объединяются:

export const handlers = [
  ...postsHandlers,
  ...authHandlers,
  ...commentsHandlers
];

Это сохраняет управляемость моков при большом числе RTK Query endpoints.