Мокирование запросов

RTK Query предоставляет встроенные механизмы для подмены сетевого слоя и тестирования API без реальных HTTP-запросов. Мокирование запросов используется для изоляции бизнес-логики от инфраструктуры, ускорения тестов и создания предсказуемого поведения API в разработке и CI.

RTK Query строится поверх Redux Toolkit и использует единый слой API-сервиса, который инкапсулирует логику запросов. Основная точка расширения для мокирования — baseQuery.

baseQuery представляет собой функцию, которая принимает описание запроса и возвращает результат. Именно через её замену реализуется подмена реальных HTTP-вызовов.

Типичная сигнатура:

const baseQuery = async (args, api, extraOptions) => {
  return { data: result }
}

Мокирование сводится к созданию альтернативного baseQuery, который возвращает заранее определённые данные вместо обращения к серверу.


Простейшее мокирование через кастомный baseQuery

Наиболее прямолинейный способ — заменить fetchBaseQuery собственной реализацией.

import { createApi } fr om '@reduxjs/toolkit/query/react'

const mockBaseQuery = async (args) => {
  if (args.url === '/users') {
    return {
      data: [
        { id: 1, name: 'Alex' },
        { id: 2, name: 'Maria' }
      ]
    }
  }

  return { error: { status: 404, data: 'Not found' } }
}

export const api = createApi({
  reducerPath: 'api',
  baseQuery: mockBaseQuery,
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => ({ url: '/users', method: 'GET' })
    })
  })
})

В этом варианте логика мокирования полностью сосредоточена в одном месте. Поведение API становится детерминированным.


Разделение моков по средам выполнения

Часто требуется различать поведение в зависимости от окружения: development, test, storybook.

const realBaseQuery = fetchBaseQuery({
  baseUrl: 'https://api.example.com'
})

const mockBaseQuery = async (args) => {
  switch (args.url) {
    case '/users':
      return { data: [{ id: 1, name: 'Test User' }] }
    default:
      return { error: { status: 500, data: 'Unknown endpoint' } }
  }
}

const baseQuery =
  process.env.NODE_ENV === 'test'
    ? mockBaseQuery
    : realBaseQuery

Такой подход позволяет сохранять один API слой при разных источниках данных.


Мокирование через fake latency и имитацию сети

Для приближения к реальному поведению API добавляется искусственная задержка.

const delay = (ms) => new Promise(res => setTimeout(res, ms))

const mockBaseQuery = async (args) => {
  await delay(300)

  if (args.url === '/users') {
    return {
      data: [
        { id: 1, name: 'Delayed User' }
      ]
    }
  }

  return { error: { status: 404, data: 'Not found' } }
}

Искусственная задержка позволяет тестировать состояния загрузки (isLoading, isFetching) и UI-реакции на асинхронность.


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

RTK Query позволяет модифицировать ответ через transformResponse. Это даёт возможность мокировать данные на уровне конкретного endpoint, не затрагивая baseQuery.

getUsers: builder.query({
  query: () => '/users',
  transformResponse: (response) => {
    return [
      ...response,
      { id: 999, name: 'Injected User' }
    ]
  }
})

Этот подход полезен, когда требуется частичное изменение данных без полной подмены API слоя.


Мокирование через endpoint injection

RTK Query поддерживает динамическое расширение API через injectEndpoints. Это позволяет создавать отдельные мок-слои поверх базового API.

const mockApi = api.injectEndpoints({
  endpoints: (builder) => ({
    getUsersMock: builder.query({
      queryFn: async () => {
        return {
          data: [
            { id: 1, name: 'Injected Mock' }
          ]
        }
      }
    })
  })
})

queryFn полностью заменяет baseQuery для конкретного endpoint, что делает его идеальным инструментом для точечного мокирования.


Использование queryFn как полного bypass сетевого слоя

queryFn является альтернативой query и даёт полный контроль над логикой запроса.

getUserById: builder.query({
  queryFn: async (id) => {
    if (id === 1) {
      return { data: { id: 1, name: 'Local User' } }
    }

    return { error: { status: 404, data: 'User not found' } }
  }
})

Этот способ полностью исключает baseQuery, включая любые middleware-эффекты.


Мокирование через dependency injection baseQueryFactory

Для масштабных приложений применяется фабрика baseQuery, позволяющая подменять зависимости.

const createBaseQuery = ({ mock }) => {
  if (mock) {
    return async (args) => {
      return { data: { mock: true, args } }
    }
  }

  return fetchBaseQuery({ baseUrl: '/api' })
}

const api = createApi({
  baseQuery: createBaseQuery({ mock: true }),
  endpoints: (builder) => ({
    getData: builder.query({
      query: () => '/data'
    })
  })
})

Такой подход обеспечивает централизованное управление режимами работы API.


Мокирование ошибок и edge-case сценариев

Тестирование API невозможно без симуляции ошибок: таймаутов, 500-х статусов, некорректных данных.

const mockBaseQuery = async (args) => {
  if (args.url === '/timeout') {
    return {
      error: {
        status: 'TIMEOUT',
        data: 'Request exceeded time lim it'
      }
    }
  }

  if (args.url === '/server-error') {
    return {
      error: {
        status: 500,
        data: 'Internal Server Error'
      }
    }
  }

  return { data: {} }
}

Такая модель позволяет проверять устойчивость UI и корректность обработки error состояния в RTK Query.


Мокирование с сохранением кэш-механики RTK Query

RTK Query кэширует данные по ключам endpoint + args. При мокировании важно сохранять стабильность возвращаемых значений, иначе кэш может вести себя непредсказуемо.

const mockUsers = [
  { id: 1, name: 'Static User' }
]

const mockBaseQuery = async () => {
  return {
    data: mockUsers
  }
}

Стабильные ссылки на данные позволяют корректно тестировать селекторы и мемоизацию.


Мокирование с использованием MSW как внешнего слоя

Хотя RTK Query поддерживает встроенное мокирование, часто применяется Mock Service Worker как перехватчик сетевых запросов.

import { rest } from 'msw'

export const handlers = [
  rest.get('/users', (req, res, ctx) => {
    return res(
      ctx.json([
        { id: 1, name: 'MSW User' }
      ])
    )
  })
]

RTK Query при этом остаётся неизменённым, а подмена происходит на уровне сети, что ближе к реальному поведению браузера.


Изоляция моков в тестовой среде Redux store

При тестировании RTK Query используется конфигурация store с подменённым API slice.

import { configureStore } from '@reduxjs/toolkit'
import { api } from './api'

const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware)
})

Если baseQuery мокирован, весь store автоматически работает в тестовом режиме без изменений компонентов.


Комбинированные стратегии мокирования

На практике часто используется комбинация:

  • baseQuery для глобального мокирования
  • queryFn для точечных кейсов
  • transformResponse для локальных изменений
  • MSW для интеграционного уровня

Такое разделение позволяет моделировать разные уровни реальности API без дублирования логики.


Типовые ошибки при мокировании RTK Query

Неправильная структура ответа baseQuery приводит к некорректной работе RTK Query. Всегда требуется строгое соответствие одному из вариантов:

  • { data: ... }
  • { error: ... }

Возврат произвольных объектов ломает нормализацию состояния.

Другой частой ошибкой является изменение структуры данных между вызовами, что приводит к лишним перерендерингам и сбросу кэша.


Поведение middleware при мокировании

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

  • onQueryStarted вызывается всегда
  • cache lifecycle сохраняется
  • invalidatesTags работает как обычно

Мокирование не отключает внутреннюю логику Redux, а лишь подменяет источник данных.