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

Интеграционное тестирование RTK Query строится вокруг проверки взаимодействия нескольких уровней системы: Redux store, API slice, middleware, кеширования и сетевого слоя. В отличие от юнит-тестов, где изолируется отдельная функция или редьюсер, здесь проверяется поведение системы как единого механизма — от инициализации запроса до попадания данных в кеш и последующего чтения из store.

RTK Query тесно связан с Redux Toolkit и работает через middleware, что делает его особенно удобным для интеграционных сценариев. Основная цель тестирования — подтвердить корректность цепочки:

инициация запроса → middleware обработка → выполнение запроса (или mock) → нормализация ответа → запись в cache → селекторы → реактивное обновление состояния


Подготовка тестового окружения

Интеграционные тесты RTK Query обычно строятся на реальном Redux store с подключённым API slice. При этом сетевой слой заменяется моками через fetchBaseQuery или через глобальный fetch.

Ключевые зависимости:

  • Redux Toolkit
  • RTK Query API slice
  • Jest или Vitest
  • MSW (Mock Service Worker) либо ручное мокирование fetch

Конфигурация store

Для интеграционного тестирования важно использовать максимально приближённую к реальной конфигурацию store.

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

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

Здесь принципиально важно подключение api.middleware, так как именно он управляет жизненным циклом запросов, кешированием и инвалидированием данных.


Мокирование сетевого слоя

Использование MSW

MSW позволяет перехватывать реальные HTTP-запросы и возвращать контролируемые ответы, не вмешиваясь в код RTK Query.

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

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

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

Подключение жизненного цикла тестов

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

Тестирование запроса через store.dispatch

RTK Query позволяет вызывать endpoints напрямую через dispatch.

import { createTestStore } from './store';
import { api } from '../services/api';

test('fetch users loads data into store', async () => {
  const store = createTestStore();

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

  const state = store.getState();

  expect(result.data).toEqual([{ id: 1, name: 'Alex' }]);
  expect(state.api.queries['getUsers(undefined)'].status).toBe('fulfilled');
});

Проверка кеширования

Одним из ключевых аспектов RTK Query является кеширование результатов. Интеграционное тестирование должно проверять, что повторный запрос не приводит к лишнему сетевому вызову.

test('caches query result and avoids refetch', async () => {
  const store = createTestStore();

  await store.dispatch(api.endpoints.getUsers.initiate());
  await store.dispatch(api.endpoints.getUsers.initiate());

  const queries = Object.keys(store.getState().api.queries);

  expect(queries.length).toBe(1);
});

Поведение кеша зависит от keepUnusedDataFor и refetchOnMountOrArgChange, поэтому в тестах важно фиксировать эти параметры в API slice.


Тестирование автоматического refetch

RTK Query может автоматически обновлять данные при:

  • монтировании нового подписчика
  • изменении аргументов
  • инвалидировании тегов
test('refetch on tag invalidation', async () => {
  const store = createTestStore();

  await store.dispatch(api.endpoints.getUsers.initiate());

  store.dispatch(
    api.util.invalidateTags([{ type: 'User', id: 'LIST' }])
  );

  const state = store.getState();
  const query = state.api.queries['getUsers(undefined)'];

  expect(query?.endpointName).toBe('getUsers');
});

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

RTK Query управляет состояниями:

  • pending
  • fulfilled
  • rejected

Интеграционные тесты часто проверяют переходы между ними.

test('loading states transition correctly', async () => {
  const store = createTestStore();

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

  let state = store.getState();
  expect(state.api.queries['getUsers(undefined)']?.status).toBe('pending');

  await promise;

  state = store.getState();
  expect(state.api.queries['getUsers(undefined)']?.status).toBe('fulfilled');
});

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

Ошибочные сценарии критичны для проверки устойчивости интеграции.

test('handles request error', async () => {
  server.use(
    rest.get('/users', (req, res, ctx) => {
      return res(ctx.status(500));
    })
  );

  const store = createTestStore();

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

  expect(result.error).toBeDefined();
  expect(result.error.status).toBe(500);
});

Интеграция с React (через hooks)

Хотя интеграционные тесты чаще фокусируются на store, RTK Query активно используется через hooks.

import { renderHook, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux';
import { createTestStore } from './store';
import { useGetUsersQuery } from '../services/api';

test('hook returns data from API', async () => {
  const store = createTestStore();

  const wrapper = ({ children }) => (
    <Provider store={store}>{children}</Provider>
  );

  const { result } = renderHook(() => useGetUsersQuery(), {
    wrapper,
  });

  await waitFor(() => {
    expect(result.current.data).toBeDefined();
  });
});

Здесь проверяется полная цепочка: hook → middleware → request → store → rerender.


Проверка дедупликации запросов

RTK Query предотвращает дублирование одинаковых запросов.

test('deduplicates identical requests', async () => {
  const store = createTestStore();

  const promise1 = store.dispatch(api.endpoints.getUsers.initiate());
  const promise2 = store.dispatch(api.endpoints.getUsers.initiate());

  await Promise.all([promise1, promise2]);

  const state = store.getState();
  const queryState = state.api.queries['getUsers(undefined)'];

  expect(queryState?.requestId).toBeDefined();
});

Проверка lifecycle подписок

RTK Query хранит данные пока есть активные подписчики.

test('removes cache after last unsubscribe', async () => {
  const store = createTestStore();

  const subscription = store.dispatch(
    api.endpoints.getUsers.initiate()
  );

  subscription.unsubscribe();

  const state = store.getState();

  expect(state.api.queries['getUsers(undefined)']).toBeUndefined();
});

Тестирование с несколькими endpoints

Интеграционные сценарии часто включают цепочки запросов.

test('handles multiple endpoints interaction', async () => {
  const store = createTestStore();

  await store.dispatch(api.endpoints.getUsers.initiate());
  await store.dispatch(api.endpoints.getUserById.initiate(1));

  const state = store.getState();

  expect(state.api.queries['getUsers(undefined)']).toBeDefined();
  expect(state.api.queries['getUserById(1)']).toBeDefined();
});

Инвалидация и перекрёстное обновление данных

RTK Query использует теги для синхронизации данных между endpoint’ами.

test('updates related queries via tags', async () => {
  const store = createTestStore();

  await store.dispatch(api.endpoints.getUsers.initiate());

  store.dispatch(
    api.util.invalidateTags([{ type: 'User', id: 1 }])
  );

  const state = store.getState();

  expect(state.api.queries['getUsers(undefined)']?.isFetching).toBe(true);
});

Контроль стабильности интеграции

Интеграционные тесты RTK Query обычно направлены на проверку следующих свойств системы:

  • корректная работа middleware
  • целостность кеша
  • согласованность состояния store
  • синхронизация между endpoint’ами
  • предсказуемость жизненного цикла запросов
  • устойчивость к ошибкам сети
  • отсутствие дублирования запросов

Каждый из этих аспектов проверяется через взаимодействие нескольких слоёв системы, а не через изолированные функции или модули