Тестирование React-компонентов, использующих TanStack Query, требует учета асинхронной природы данных, кэширования, повторных запросов и поведения QueryClient. В связке с React Testing Library основной задачей становится контроль сетевого слоя, управление жизненным циклом QueryClient и детерминированная имитация серверных ответов.
Базовая проблема при тестировании компонентов с TanStack Query заключается в том, что QueryClient живет вне React-компонента и управляет кэшем глобально. Это приводит к утечкам состояния между тестами, если не изолировать экземпляры.
Ключевой подход — создание фабрики QueryClient для каждого теста.
import { QueryClient } from '@tanstack/react-query';
export function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
staleTime: 0,
},
},
});
}
Отключение retry критично: иначе тесты могут флапать из-за повторных попыток запроса.
Компоненты с использованием useQuery требуют QueryClientProvider. В тестовой среде удобнее создать кастомный render.
import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { render } from '@testing-library/react';
import { createTestQueryClient } from './createTestQueryClient';
export function renderWithQuery(ui) {
const queryClient = createTestQueryClient();
function Wrapper({ children }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
return {
queryClient,
...render(ui, { wrapper: Wrapper }),
};
}
Такой подход позволяет в тестах напрямую управлять кэшем через returned queryClient.
Компоненты с useQuery почти всегда имеют состояние loading. React Testing Library хорошо работает с этим через findBy queries.
import { screen } from '@testing-library/react';
import { renderWithQuery } from './renderWithQuery';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.json([{ id: 1, name: 'Alex' }]));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('renders users after loading', async () => {
renderWithQuery(<Users />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
const user = await screen.findByText('Alex');
expect(user).toBeInTheDocument();
});
Использование MSW позволяет тестировать слой запросов без моков fetch или axios.
Стабильность тестов обеспечивается тем, что сетевые ответы контролируются через Mock Service Worker.
Можно тестировать как успешные, так и ошибочные сценарии.
server.use(
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.status(500));
})
);
test('renders error state', async () => {
renderWithQuery(<Users />);
const error = await screen.findByText(/error/i);
expect(error).toBeInTheDocument();
});
Одно из ключевых преимуществ TanStack Query — кэш. В тестах его поведение часто мешает, если не управлять им явно.
QueryClient позволяет напрямую модифицировать состояние:
test('uses cached data', async () => {
const { queryClient, rerender } = renderWithQuery(<Users />);
queryClient.setQueryData(['users'], [
{ id: 1, name: 'Cached User' },
]);
rerender(<Users />);
const user = await screen.findByText('Cached User');
expect(user).toBeInTheDocument();
});
Такой подход полезен для проверки UI без повторного запроса.
Mutation часто вызывает invalidateQueries, и это поведение важно проверять.
import { useMutation, useQueryClient } from '@tanstack/react-query';
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (user) =>
fetch('/api/users', {
method: 'POST',
body: JSON.stringify(user),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
Тестирование:
test('invalidates users query after mutation', async () => {
const { queryClient } = renderWithQuery(<Users />);
queryClient.setQueryData(['users'], [{ id: 1, name: 'Old' }]);
// имитация invalidation
queryClient.invalidateQueries({ queryKey: ['users'] });
expect(queryClient.isFetching()).toBe(true);
});
React Testing Library ориентирована на поведение пользователя, поэтому мутации тестируются через события.
test('creates new user', async () => {
renderWithQuery(<CreateUserForm />);
userEvent.type(screen.getByPlaceholderText('Name'), 'John');
userEvent.click(screen.getByText('Create'));
const user = await screen.findByText('John');
expect(user).toBeInTheDocument();
});
При этом важно контролировать backend через MSW, иначе тест становится нестабильным.
TanStack Query по умолчанию может повторять запросы при ошибках. В тестовой среде это приводит к непредсказуемому поведению.
Настройки:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
},
},
});
Также важно отключать refetchOnMount, если тест проверяет только UI-состояние.
Поведение stale-данных часто становится источником скрытых багов.
queryClient.setQueryData(['users'], data);
queryClient.invalidateQueries(['users']);
После этого useQuery должен перейти в состояние fetching, сохранив предыдущие данные. Это можно проверять через UI:
expect(screen.getByText('Old Data')).toBeInTheDocument();
expect(await screen.findByText('Updated Data')).toBeInTheDocument();
React Testing Library предоставляет waitFor для синхронизации с TanStack Query.
import { waitFor } from '@testing-library/react';
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
});
waitFor особенно важен при сложных зависимостях queries, когда один запрос зависит от результата другого.
Общий QueryClient между тестами приводит к утечке состояния кэша.
Правильная стратегия — новый клиент на каждый render:
Это устраняет эффект “памяти” между тестами.
Когда один query зависит от другого:
useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
enabled: !!id,
});
Тестирование должно учитывать состояние enabled:
test('does not fetch without id', () => {
renderWithQuery(<User userId={null} />);
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
TanStack Query разделяет:
В тестах важно различать эти состояния.
expect(queryClient.isFetching()).toBe(0);
или через UI:
expect(screen.getByText(/loading/i)).toBeInTheDocument();
Стабильная стратегия тестирования строится на трех слоях:
Эта комбинация устраняет необходимость мокать fetch вручную и снижает связанность тестов с реализацией.
Эти ошибки приводят к нестабильным тестам и ложным падениям.
Если включены refetchInterval или background updates, тесты могут становиться недетерминированными.
Решение — отключение интервалов в тестовой конфигурации:
refetchInterval: false
или полное управление таймерами через fake timers при необходимости.
Optimistic updates требуют проверки промежуточного состояния UI.
onMutate: async (newUser) => {
queryClient.setQueryData(['users'], (old) => [...old, newUser]);
}
Тест:
userEvent.click(screen.getByText('Create'));
expect(screen.getByText('Temp User')).toBeInTheDocument();
Тестирование с React Testing Library и TanStack Query строится вокруг управления состоянием QueryClient, контроля сети через MSW и проверки пользовательского поведения вместо внутренних механизмов библиотеки.