Тестирование компонентов, использующих RTK Query, требует учета особенностей встроенного кэширования, асинхронных запросов и тесной интеграции с Redux store. В отличие от обычных Redux-слайсов, RTK Query добавляет слой абстракции над сетевыми запросами, поэтому тесты должны контролировать не только UI, но и поведение API-слоя, состояние запроса и кэш.
Базовая точка тестирования компонентов с RTK Query — корректно
сконфигурированный store. В него обязательно подключается reducer
API-сервиса и middleware, иначе хуки useQuery и
useMutation не смогут функционировать корректно.
import { configureStore } from '@reduxjs/toolkit';
import { api } from '../services/api';
export function setupStore(preloadedState) {
return configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
preloadedState,
});
}
Ключевой момент — использование api.reducerPath как
динамического ключа. Это обеспечивает совместимость с любым API
slice.
Для тестирования React-компонентов необходимо обернуть их в
Provider с тестовым store.
import React from 'react';
import { Provider } from 'react-redux';
import { setupStore } from './setupStore';
export function renderWithStore(ui, preloadedState) {
const store = setupStore(preloadedState);
return {
store,
...render(<Provider store={store}>{ui}</Provider>),
};
}
Такой подход позволяет контролировать состояние Redux внутри каждого теста и при необходимости сбрасывать его между сценариями.
Наиболее устойчивый способ тестирования RTK Query — использование Mock Service Worker. Он перехватывает реальные HTTP-запросы и возвращает заранее определенные ответы.
import { rest } from 'msw';
import { setupServer } from 'msw/node';
export const handlers = [
rest.get('/users', (req, res, ctx) => {
return res(ctx.json([{ id: 1, name: 'Alex' }]));
}),
];
export const server = setupServer(...handlers);
В тестовой среде сервер запускается и останавливается:
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
MSW позволяет тестировать RTK Query как в реальном приложении, сохраняя поведение сети.
Компоненты с useQuery обычно имеют три ключевых
состояния: загрузка, успех и ошибка. Состояние загрузки можно
тестировать через ожидание появления индикатора.
test('отображает loader во время загрузки', async () => {
renderWithStore(<UsersList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
Важно учитывать, что RTK Query может кешировать данные, и повторный рендер может не вызывать загрузку. Для изоляции теста иногда используется сброс API состояния:
store.dispatch(api.util.resetApiState());
После завершения запроса компонент должен отрендерить данные из кэша RTK Query.
test('рендерит список пользователей', async () => {
renderWithStore(<UsersList />);
const item = await screen.findByText('Alex');
expect(item).toBeInTheDocument();
});
findByText используется, так как RTK Query выполняет
асинхронный запрос, и UI обновляется после завершения промиса.
Ошибки сети можно симулировать через MSW, переопределяя handler.
server.use(
rest.get('/users', (req, res, ctx) => {
return res(ctx.status(500));
})
);
Тест проверяет корректный UI-рендер ошибки:
test('показывает ошибку при неудачном запросе', async () => {
renderWithStore(<UsersList />);
const error = await screen.findByText(/error/i);
expect(error).toBeInTheDocument();
});
Mutations требуют отдельного подхода, так как они инициируются вручную.
const [createUser] = useCreateUserMutation();
В тесте важно вызвать mutation и дождаться обновления состояния.
test('создает пользователя', async () => {
renderWithStore(<CreateUserForm />);
fireEvent.change(screen.getByPlaceholderText('name'), {
target: { value: 'John' },
});
fireEvent.click(screen.getByText('submit'));
expect(await screen.findByText('success')).toBeInTheDocument();
});
MSW используется для симуляции POST-запроса:
rest.post('/users', (req, res, ctx) => {
return res(ctx.json({ id: 2, name: 'John' }));
});
Иногда требуется тестировать хуки без UI. Для этого используется
renderHook из React Testing Library.
import { renderHook } from '@testing-library/react';
import { Provider } from 'react-redux';
test('useGetUsersQuery возвращает данные', async () => {
const store = setupStore();
const wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
);
const { result } = renderHook(() => useGetUsersQuery(), { wrapper });
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
});
RTK Query активно использует кэш, поэтому один и тот же запрос может не повторяться между тестами. Это часто приводит к «ложно успешным» результатам.
Для полной изоляции используется:
afterEach(() => {
store.dispatch(api.util.resetApiState());
});
Также можно отключать кэширование через
refetchOnMountOrArgChange, если требуется строгая проверка
каждого запроса.
selectFromResult позволяет оптимизировать рендер, но
усложняет тестирование, так как возвращаемое значение отличается от
стандартного ответа.
const { data, isLoading } = useGetUsersQuery(undefined, {
selectFromResult: ({ data, isLoading }) => ({
data: data?.slice(0, 5),
isLoading,
}),
});
Тесты должны учитывать трансформацию данных:
expect(screen.getAllByRole('listitem')).toHaveLength(5);
Middleware RTK Query может влиять на глобальное состояние store. Для стабильных тестов важно избегать шаринга store между тестами.
let store;
beforeEach(() => {
store = setupStore();
});
Каждый тест получает новый экземпляр store, что исключает утечки состояния.
Помимо MSW, можно мокировать сам API slice:
jest.mock('../services/api', () => ({
useGetUsersQuery: () => ({
data: [{ id: 1, name: 'Mock' }],
isLoading: false,
isSuccess: true,
}),
}));
Такой подход ускоряет тесты, но снижает их реалистичность, поэтому применяется только для изолированных unit-тестов компонентов UI.
RTK Query автоматически дедуплицирует запросы с одинаковыми аргументами. Это можно проверять через spy на fetch:
const fetchSpy = jest.spyOn(global, 'fetch');
renderWithStore(<UsersList />);
renderWithStore(<UsersList />);
expect(fetchSpy).toHaveBeenCalledTimes(1);
skipTokenskipToken используется для условного отключения
запроса.
useGetUserQuery(userId ?? skipToken);
Тест проверяет, что запрос не выполняется:
renderWithStore(<UserProfile userId={null} />);
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
Компоненты, использующие несколько RTK Query хуков, требуют проверки согласованности состояний.
const user = useGetUserQuery(id);
const posts = useGetPostsQuery(id);
Тест должен учитывать, что данные приходят независимо:
await screen.findByText('User loaded');
await screen.findByText('Posts loaded');
Сценарии ошибок также могут быть раздельными, что важно проверять отдельно.
RTK Query позволяет инвалидировать теги после mutation, и это поведение должно проверяться в тестах.
server.use(
rest.post('/users', (req, res, ctx) => {
return res(ctx.json({ id: 3, name: 'Kate' }));
})
);
После выполнения mutation список пользователей должен обновиться автоматически:
await screen.findByText('Kate');
Это подтверждает корректную работу invalidation и refetch механизма.