Юнит-тестирование API слайсов

Юнит-тестирование API-слайсов в RTK Query требует понимания того, как библиотека структурирует логику запросов, как формируются эндпоинты и какие уровни абстракции можно изолировать для проверки. RTK Query построен поверх Redux Toolkit и использует декларативное описание API через createApi, где каждый endpoint представляет собой чистую конфигурацию, а большая часть логики (кэширование, дедупликация, инвалидация) реализована внутри middleware. Это влияет на стратегию тестирования: напрямую тестировать “всё сразу” неэффективно, необходимо разделять тестирование на уровни.


API-слайс в RTK Query создаётся через createApi, который возвращает объект с несколькими ключевыми сущностями:

  • reducer и reducerPath
  • middleware
  • набор endpoints
  • auto-generated hooks

Каждая из этих частей имеет свою тестовую ценность.

Основные зоны тестирования

  1. Конфигурация endpoints
  2. Query/Mutation функции (transformRequest, transformResponse)
  3. Tag system (providesTags / invalidatesTags)
  4. Селекторы кеша
  5. Поведение при dispatch через store

Подходы к тестированию RTK Query

RTK Query можно тестировать на трёх уровнях:

Изолированное тестирование логики endpoint

Проверяется чистая функция запроса без Redux store. Например:

  • корректность формирования URL
  • правильность параметров
  • трансформация данных

Тестирование через Redux store

Создаётся реальный store с подключённым API slice:

  • проверяется dispatch initiate
  • проверяется состояние cache
  • проверяется статус загрузки

Интеграционное тестирование с мокированием сети

Используются инструменты вроде msw (Mock Service Worker) для имитации HTTP:

  • проверяется полный цикл запроса
  • кэширование
  • повторные запросы

Тестирование API-слайса через store

RTK Query тесно интегрирован с Redux store, поэтому наиболее корректный способ проверки поведения endpoints — использование реального store с подключённым reducer и middleware.

Подготовка тестового store

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

export const setupApiStore = () => {
  return configureStore({
    reducer: {
      [api.reducerPath]: api.reducer,
    },
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware().concat(api.middleware),
  });
};

Проверка query через dispatch

import { api } from '../services/api';
import { setupApiStore } from './setupApiStore';

test('fetchUser query выполняется и сохраняет данные в cache', async () => {
  const store = setupApiStore();

  const action = store.dispatch(
    api.endpoints.getUser.initiate(1)
  );

  await action.unwrap();

  const state = store.getState();

  const data = api.endpoints.getUser.select(1)(state);

  expect(data?.data).toBeDefined();
});

Проверка состояния загрузки и ошибок

RTK Query хранит метаданные запроса: isLoading, isSuccess, isError.

Тестирование состояния загрузки

test('loading state устанавливается корректно', async () => {
  const store = setupApiStore();

  const promise = store.dispatch(
    api.endpoints.getUser.initiate(1)
  );

  const stateDuring = store.getState();
  const queryState = api.endpoints.getUser.select(1)(stateDuring);

  expect(queryState?.status).toBe('pending');

  await promise.unwrap();
});

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

При использовании mock-слоя можно имитировать ошибку:

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/user/1', (req, res, ctx) => {
    return res(ctx.status(500));
  })
);

beforeAll(() => server.listen());
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
test('обработка ошибки запроса', async () => {
  const store = setupApiStore();

  const result = store.dispatch(
    api.endpoints.getUser.initiate(1)
  );

  try {
    await result.unwrap();
  } catch (error) {
    expect(error.status).toBe(500);
  }
});

Тестирование transformResponse и transformErrorResponse

RTK Query позволяет модифицировать данные до попадания в cache.

Пример API-слайса

getUser: builder.query({
  query: (id) => `/user/${id}`,
  transformResponse: (response) => {
    return {
      ...response,
      fullName: `${response.firstName} ${response.lastName}`,
    };
  },
});

Тестирование трансформации

test('transformResponse корректно преобразует данные', async () => {
  const store = setupApiStore();

  const result = await store.dispatch(
    api.endpoints.getUser.initiate(1)
  ).unwrap();

  expect(result.fullName).toBeDefined();
});

Тестирование кеширования RTK Query

Одно из ключевых свойств RTK Query — автоматическое кеширование запросов.

Проверка повторного использования cache

test('повторный запрос использует cache', async () => {
  const store = setupApiStore();

  await store.dispatch(api.endpoints.getUser.initiate(1)).unwrap();

  const firstState = store.getState();

  await store.dispatch(api.endpoints.getUser.initiate(1)).unwrap();

  const secondState = store.getState();

  expect(firstState).toBe(secondState);
});

Фактически сравнивается отсутствие повторного сетевого запроса и сохранение данных в cache.


Тестирование invalidation через tags

RTK Query использует систему тегов для автоматической инвалидации cache.

Пример API

getUser: builder.query({
  query: (id) => `/user/${id}`,
  providesTags: (result, error, id) => [{ type: 'User', id }],
}),

updateUser: builder.mutation({
  query: (data) => ({
    url: `/user/${data.id}`,
    method: 'PUT',
    body: data,
  }),
  invalidatesTags: (result, error, data) => [
    { type: 'User', id: data.id },
  ],
});

Тестирование invalidation

test('mutation инвалидирует cache query', async () => {
  const store = setupApiStore();

  await store.dispatch(api.endpoints.getUser.initiate(1)).unwrap();

  await store.dispatch(
    api.endpoints.updateUser.initiate({ id: 1, name: 'New' })
  ).unwrap();

  const state = store.getState();

  const cached = api.endpoints.getUser.select(1)(state);

  expect(cached?.data).toBeUndefined();
});

Тестирование селекторов RTK Query

RTK Query генерирует селекторы для доступа к cache:

api.endpoints.getUser.select(1)

Проверка селектора

test('selector возвращает данные из cache', () => {
  const store = setupApiStore();

  store.dispatch(
    api.util.upsertQueryData('getUser', 1, {
      id: 1,
      name: 'Test',
    })
  );

  const state = store.getState();

  const data = api.endpoints.getUser.select(1)(state);

  expect(data.data.name).toBe('Test');
});

Тестирование API без сети (unit-first подход)

Для чистого unit-тестирования можно полностью отключить сетевые вызовы через:

  • fetchBaseQuery mock
  • переопределение baseQuery

Пример мокирования baseQuery

const baseQuery = async () => {
  return {
    data: { id: 1, name: 'Mock User' },
  };
};

export const api = createApi({
  reducerPath: 'api',
  baseQuery,
  endpoints: (builder) => ({
    getUser: builder.query({
      query: () => '/user',
    }),
  }),
});

Тест

test('API возвращает мокированные данные', async () => {
  const store = setupApiStore();

  const result = await store.dispatch(
    api.endpoints.getUser.initiate()
  ).unwrap();

  expect(result.name).toBe('Mock User');
});

Тестирование побочных эффектов middleware

Middleware RTK Query управляет жизненным циклом запросов. В тестах можно проверять:

  • dispatch lifecycle actions
  • cache lifecycle
  • polling behavior

Проверка lifecycle actions

test('dispatch lifecycle actions', async () => {
  const store = setupApiStore();

  const dispatched = [];

  store.subscribe(() => {
    dispatched.push(store.getState());
  });

  await store.dispatch(api.endpoints.getUser.initiate(1)).unwrap();

  expect(dispatched.length).toBeGreaterThan(0);
});

Изоляция API слоя

При масштабных приложениях API-слайсы часто делятся по доменам:

  • authApi
  • userApi
  • productApi

Каждый slice тестируется отдельно, но через одинаковый harness:

  • общий setupApiStore
  • единая стратегия mock server
  • изоляция reducerPath

Проверка типовой структуры endpoint

Хотя JavaScript не даёт строгой типизации, можно тестировать контракт endpoint:

test('endpoint имеет корректную структуру', () => {
  const endpoint = api.endpoints.getUser;

  expect(typeof endpoint.initiate).toBe('function');
  expect(typeof endpoint.select).toBe('function');
});

Ошибки тестирования RTK Query

На практике часто встречаются проблемы:

1. Попытка тестировать hooks напрямую

RTK Query hooks требуют React environment, и без @testing-library/react или renderHook тесты становятся нестабильными.

2. Игнорирование middleware

Без подключения api.middleware cache и lifecycle не работают.

3. Тестирование только UI вместо API слоя

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


Стратегия покрытия API слоя

Полноценное тестирование включает:

  • unit тесты transformResponse
  • store тесты query/mutation
  • integration тесты с MSW
  • selector tests
  • cache invalidation tests

Такой подход позволяет проверять не только корректность запросов, но и поведение всей системы данных внутри Redux store, включая кэш, инвалидацию и жизненный цикл запросов.