Юнит-тестирование API-слайсов в RTK Query требует понимания того, как
библиотека структурирует логику запросов, как формируются эндпоинты и
какие уровни абстракции можно изолировать для проверки. RTK Query
построен поверх Redux Toolkit и использует декларативное описание API
через createApi, где каждый endpoint представляет собой
чистую конфигурацию, а большая часть логики (кэширование, дедупликация,
инвалидация) реализована внутри middleware. Это влияет на стратегию
тестирования: напрямую тестировать “всё сразу” неэффективно, необходимо
разделять тестирование на уровни.
API-слайс в RTK Query создаётся через createApi, который
возвращает объект с несколькими ключевыми сущностями:
Каждая из этих частей имеет свою тестовую ценность.
RTK Query можно тестировать на трёх уровнях:
Проверяется чистая функция запроса без Redux store. Например:
Создаётся реальный store с подключённым API slice:
initiateИспользуются инструменты вроде msw (Mock Service Worker)
для имитации HTTP:
RTK Query тесно интегрирован с Redux store, поэтому наиболее корректный способ проверки поведения endpoints — использование реального store с подключённым reducer и middleware.
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),
});
};
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);
}
});
RTK Query позволяет модифицировать данные до попадания в cache.
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 — автоматическое кеширование запросов.
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.
RTK Query использует систему тегов для автоматической инвалидации cache.
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 },
],
});
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 генерирует селекторы для доступа к 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');
});
Для чистого unit-тестирования можно полностью отключить сетевые вызовы через:
fetchBaseQuery mockconst 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 RTK Query управляет жизненным циклом запросов. В тестах можно проверять:
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-слайсы часто делятся по доменам:
Каждый slice тестируется отдельно, но через одинаковый harness:
Хотя JavaScript не даёт строгой типизации, можно тестировать контракт endpoint:
test('endpoint имеет корректную структуру', () => {
const endpoint = api.endpoints.getUser;
expect(typeof endpoint.initiate).toBe('function');
expect(typeof endpoint.select).toBe('function');
});
На практике часто встречаются проблемы:
RTK Query hooks требуют React environment, и без
@testing-library/react или renderHook тесты
становятся нестабильными.
Без подключения api.middleware cache и lifecycle не
работают.
RTK Query требует разделения: API слой тестируется отдельно от компонентов.
Полноценное тестирование включает:
Такой подход позволяет проверять не только корректность запросов, но и поведение всей системы данных внутри Redux store, включая кэш, инвалидацию и жизненный цикл запросов.