🗓️ 08062026 1400

TANSTACK QUERY TESTING

Quick reference for testing components and hooks that use tanstack_query. The main gotcha: shared cache between tests causes flaky results.

Test QueryClient​

Create a fresh client per test with retries disabled and cache kept alive:

function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: Infinity,
},
mutations: {
retry: false,
},
},
})
}

Why these settings​

SettingReason
retry: falseTests fail fast instead of waiting through 3 retries with backoff
gcTime: InfinityCache sticks around for assertions; no surprise cleanup mid-test

Test Wrapper​

Every test needs a QueryClientProvider. Create a reusable wrapper:

import { render, RenderOptions } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactNode } from 'react'

function renderWithQuery(ui: ReactNode, options?: Omit<RenderOptions, 'wrapper'>) {
const client = createTestQueryClient()

const Wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={client}>
{children}
</QueryClientProvider>
)

return { ...render(ui, { wrapper: Wrapper, ...options }), client }
}

Testing Components​

import { screen, waitFor } from '@testing-library/react'

test('renders todo list', async () => {
// MSW or similar intercepts the API call
renderWithQuery(<TodoList />)

await waitFor(() => {
expect(screen.getByText('Buy milk')).toBeInTheDocument()
})
})

Testing Custom Hooks​

Use renderHook from @testing-library/react:

import { renderHook, waitFor } from '@testing-library/react'

test('useTodos returns data', async () => {
const client = createTestQueryClient()

const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={client}>
{children}
</QueryClientProvider>
)

const { result } = renderHook(() => useTodos(), { wrapper })

await waitFor(() => {
expect(result.current.isSuccess).toBe(true)
})

expect(result.current.data).toHaveLength(3)
})

Mocking API Calls​

Intercepts at the network level; your queryFn runs unchanged:

import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'

const server = setupServer(
http.get('/api/todos', () => {
return HttpResponse.json([
{ id: 1, title: 'Buy milk' },
])
})
)

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

Direct queryFn mock​

Simpler but couples tests to implementation:

const { result } = renderHook(
() => useQuery({
queryKey: ['todos'],
queryFn: vi.fn().mockResolvedValue([{ id: 1, title: 'Buy milk' }]),
}),
{ wrapper }
)

Common Test Failures​

SymptomCauseFix
Test passes alone, fails in suiteShared QueryClient leaking cacheFresh QueryClient per test
Test times outRetry delays (1s, 2s, 4s backoff)Set retry: false
Data is undefined after renderDidn't await waitFor for async fetchWrap assertion in waitFor
Stale data from previous testCache not clearedNew QueryClient per test (not .clear())

References​