Тестирование компонентов, использующих useQuery, требует
воспроизводимого окружения, в котором управление состоянием кэша,
запросами и жизненным циклом React не зависит от реального сетевого
слоя. Основная цель — изолировать поведение компонента от внешних
факторов и обеспечить контроль над состоянием Query Client.
Ключевой элемент инфраструктуры — QueryClient, который
создаётся отдельно для каждого теста. Это позволяет избежать утечек
состояния между тестами и гарантирует предсказуемость поведения.
Типовая настройка тестового обёртывания компонента выглядит следующим образом:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render } from '@testing-library/react'
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
})
}
function renderWithClient(ui) {
const queryClient = createTestQueryClient()
function Wrapper({ children }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
return {
...render(ui, { wrapper: Wrapper }),
queryClient,
}
}
Отключение повторных попыток (retry: false) и уменьшение
времени хранения кэша (gcTime: 0) упрощает тестирование,
исключая фоновые эффекты, которые могут изменять состояние между
ассерциями.
Основной сценарий useQuery — получение данных и их
отображение. Для тестирования важно контролировать функцию запроса
(queryFn), заменяя её на мок.
import { screen, waitFor } from '@testing-library/react'
import { useQuery } from '@tanstack/react-query'
async function fetchUser() {
return { id: 1, name: 'Alex' }
}
function User() {
const { data, isLoading } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
})
if (isLoading) return <div>Loading</div>
return <div>{data.name}</div>
}
Тест:
test('отображает данные пользователя после загрузки', async () => {
const { getByText } = renderWithClient(<User />)
expect(getByText('Loading')).toBeInTheDocument()
await waitFor(() => {
expect(getByText('Alex')).toBeInTheDocument()
})
})
Здесь ключевым является ожидание асинхронного обновления состояния.
waitFor используется для синхронизации с завершением
queryFn.
Вместо реальных запросов обычно применяется явное мокирование, позволяющее моделировать разные сценарии: успешный ответ, задержку, ошибку.
const mockFetchUser = jest.fn().mockResolvedValue({
id: 1,
name: 'Alex',
})
Использование:
useQuery({
queryKey: ['user'],
queryFn: mockFetchUser,
})
Такой подход позволяет проверять:
queryFnПример проверки вызова:
expect(mockFetchUser).toHaveBeenCalledTimes(1)
useQuery предоставляет несколько флагов состояния:
isLoading, isFetching, isPending
(в зависимости от версии).
Различие между ними важно при тестировании UI.
function User() {
const { data, isFetching } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
})
return (
<div>
{isFetching && <span>Загрузка...</span>}
{data && <span>{data.name}</span>}
</div>
)
}
Тест проверяет как начальное состояние, так и переход после завершения запроса:
test('показывает состояние загрузки и затем данные', async () => {
const { getByText, queryByText } = renderWithClient(<User />)
expect(getByText('Загрузка...')).toBeInTheDocument()
await waitFor(() => {
expect(queryByText('Загрузка...')).not.toBeInTheDocument()
})
})
Ошибочные сценарии являются обязательной частью тестирования React
Query компонентов. Для их симуляции queryFn должен
выбрасывать исключение.
const mockErrorFetch = jest.fn().mockRejectedValue(
new Error('Network Error')
)
Компонент:
function User() {
const { error, isError } = useQuery({
queryKey: ['user'],
queryFn: mockErrorFetch,
})
if (isError) return <div>{error.message}</div>
return <div>OK</div>
}
Тест:
test('отображает ошибку при сбое запроса', async () => {
const { getByText } = renderWithClient(<User />)
await waitFor(() => {
expect(getByText('Network Error')).toBeInTheDocument()
})
})
Важно учитывать, что React Query переводит состояние в
error асинхронно, поэтому синхронные проверки без ожидания
часто приводят к нестабильным тестам.
Одной из частых проблем является утечка кэша между тестами.
QueryClient хранит результаты запросов, поэтому без
изоляции тесты становятся зависимыми друг от друга.
Решение — создание нового клиента для каждого теста и явная очистка:
afterEach(() => {
queryClient.clear()
})
Дополнительно используется:
queryClient.removeQueries()
или
queryClient.resetQueries()
в зависимости от сценария.
React Query может возвращать данные из кэша без повторного вызова
queryFn. Это поведение важно проверять, особенно при
оптимизации производительности.
test('использует кэш при повторном рендере', async () => {
const { rerender } = renderWithClient(<User />)
await waitFor(() => {
expect(mockFetchUser).toHaveBeenCalledTimes(1)
})
rerender(<User />)
expect(mockFetchUser).toHaveBeenCalledTimes(1)
})
Если кэш настроен корректно, повторный рендер не вызывает новый запрос.
Параметр staleTime влияет на момент, когда данные
считаются устаревшими.
useQuery({
queryKey: ['user'],
queryFn: fetchUser,
staleTime: 5000,
})
При тестировании важно учитывать, что истечение времени может привести к повторным запросам.
Используется управление таймерами:
jest.useFakeTimers()
jest.advanceTimersByTime(6000)
Тест проверяет, что после истечения staleTime происходит
новый запрос:
expect(mockFetchUser).toHaveBeenCalledTimes(2)
Иногда данные уже находятся в кэше до рендера компонента. Это особенно важно при тестировании серверного рендера или предварительных загрузок.
queryClient.setQueryData(['user'], { id: 1, name: 'Alex' })
Компонент сразу получает данные без состояния загрузки:
test('использует предварительно загруженные данные', () => {
queryClient.setQueryData(['user'], { id: 1, name: 'Alex' })
const { queryByText } = renderWithClient(<User />)
expect(queryByText('Loading')).not.toBeInTheDocument()
expect(queryByText('Alex')).toBeInTheDocument()
})
useQuery предоставляет функции для принудительного
обновления данных.
function User() {
const { data, refetch } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
})
return (
<div>
<div>{data?.name}</div>
<button onCl ick={() => refetch()}>Reload</button>
</div>
)
}
Тестирование:
test('выполняет повторный запрос при refetch', async () => {
const { getByText } = renderWithClient(<User />)
await waitFor(() => {
expect(mockFetchUser).toHaveBeenCalledTimes(1)
})
getByText('Reload').click()
await waitFor(() => {
expect(mockFetchUser).toHaveBeenCalledTimes(2)
})
})
При тестировании нескольких компонентов, использующих одинаковые ключи запросов, важно учитывать пересечение кэша. В таких случаях применяются:
queryKey для тестовQueryClient на каждый тестconst queryClient = new QueryClient()
afterEach(() => {
queryClient.getQueryCache().clear()
})
Дополнительно полезно использовать детерминированные ключи:
['user', testId]
React Query может выполнять фоновый refetch при фокусе окна или монтировании.
Для тестирования используется имитация событий:
window.dispatchEvent(new Event('focus'))
И проверка изменения состояния:
await waitFor(() => {
expect(mockFetchUser).toHaveBeenCalledTimes(2)
})
Контроль таких сценариев требует отключения некоторых дефолтных поведений:
refetchOnWindowFocus: false
Основная сложность тестирования useQuery заключается в
асинхронной природе обновления состояния React Query. Простые
getBy* методы часто недостаточны без ожидания
обновлений.
Используются:
findBy* для ожидания появления элементаwaitFor для проверки состоянияact для принудительной синхронизацииawait screen.findByText('Alex')
или
await waitFor(() => {
expect(screen.getByText('Alex')).toBeInTheDocument()
})
Корректное использование этих инструментов устраняет флаки-тесты и делает поведение компонентов воспроизводимым.