# Testing TanStack Query **Testing queries, mutations, and components** --- ## Setup ```bash npm install -D @testing-library/react @testing-library/jest-dom vitest msw ``` ### Test Utils ```tsx // src/test-utils.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render } from '@testing-library/react' export function createTestQueryClient() { return new QueryClient({ defaultOptions: { queries: { retry: false, // Disable retries in tests gcTime: Infinity, }, }, logger: { log: console.log, warn: console.warn, error: () => {}, // Silence errors in tests }, }) } export function renderWithClient(ui: React.ReactElement) { const testQueryClient = createTestQueryClient() return render( {ui} ) } ``` --- ## Testing Queries ```tsx import { renderHook, waitFor } from '@testing-library/react' import { useTodos } from './useTodos' describe('useTodos', () => { it('fetches todos successfully', async () => { const { result } = renderHook(() => useTodos(), { wrapper: ({ children }) => ( {children} ), }) // Initially pending expect(result.current.isPending).toBe(true) // Wait for success await waitFor(() => expect(result.current.isSuccess).toBe(true)) // Check data expect(result.current.data).toHaveLength(3) }) it('handles errors', async () => { // Mock fetch to fail global.fetch = vi.fn(() => Promise.reject(new Error('API error')) ) const { result } = renderHook(() => useTodos()) await waitFor(() => expect(result.current.isError).toBe(true)) expect(result.current.error?.message).toBe('API error') }) }) ``` --- ## Testing with MSW ```tsx import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('/api/todos', () => { return HttpResponse.json([ { id: 1, title: 'Test todo', completed: false }, ]) }) ) beforeAll(() => server.listen()) afterEach(() => server.resetHandlers()) afterAll(() => server.close()) test('fetches todos', async () => { const { result } = renderHook(() => useTodos()) await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(result.current.data).toEqual([ { id: 1, title: 'Test todo', completed: false }, ]) }) test('handles server error', async () => { server.use( http.get('/api/todos', () => { return new HttpResponse(null, { status: 500 }) }) ) const { result } = renderHook(() => useTodos()) await waitFor(() => expect(result.current.isError).toBe(true)) }) ``` --- ## Testing Mutations ```tsx test('adds todo successfully', async () => { const { result } = renderHook(() => useAddTodo()) act(() => { result.current.mutate({ title: 'New todo' }) }) await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(result.current.data).toEqual( expect.objectContaining({ title: 'New todo' }) ) }) test('handles mutation error', async () => { server.use( http.post('/api/todos', () => { return new HttpResponse(null, { status: 400 }) }) ) const { result } = renderHook(() => useAddTodo()) act(() => { result.current.mutate({ title: 'New todo' }) }) await waitFor(() => expect(result.current.isError).toBe(true)) }) ``` --- ## Testing Components ```tsx import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { TodoList } from './TodoList' test('displays todos', async () => { renderWithClient() expect(screen.getByText(/loading/i)).toBeInTheDocument() await waitFor(() => { expect(screen.getByText('Test todo')).toBeInTheDocument() }) }) test('adds new todo', async () => { renderWithClient() await waitFor(() => { expect(screen.getByText('Test todo')).toBeInTheDocument() }) const input = screen.getByPlaceholderText(/new todo/i) const button = screen.getByRole('button', { name: /add/i }) await userEvent.type(input, 'Another todo') await userEvent.click(button) await waitFor(() => { expect(screen.getByText('Another todo')).toBeInTheDocument() }) }) ``` --- ## Testing with Prefilled Cache ```tsx test('uses prefilled cache', () => { const queryClient = createTestQueryClient() // Prefill cache queryClient.setQueryData(['todos'], [ { id: 1, title: 'Cached todo', completed: false }, ]) render( ) // Should immediately show cached data expect(screen.getByText('Cached todo')).toBeInTheDocument() }) ``` --- ## Testing Optimistic Updates ```tsx test('optimistic update rollback on error', async () => { const queryClient = createTestQueryClient() queryClient.setQueryData(['todos'], [ { id: 1, title: 'Original', completed: false }, ]) server.use( http.patch('/api/todos/1', () => { return new HttpResponse(null, { status: 500 }) }) ) const { result } = renderHook(() => useUpdateTodo(), { wrapper: ({ children }) => ( {children} ), }) act(() => { result.current.mutate({ id: 1, completed: true }) }) // Check optimistic update expect(queryClient.getQueryData(['todos'])).toEqual([ { id: 1, title: 'Original', completed: true }, ]) // Wait for rollback await waitFor(() => expect(result.current.isError).toBe(true)) // Should rollback expect(queryClient.getQueryData(['todos'])).toEqual([ { id: 1, title: 'Original', completed: false }, ]) }) ``` --- ## Best Practices ✅ Disable retries in tests ✅ Use MSW for consistent mocking ✅ Test loading, success, and error states ✅ Test optimistic updates and rollbacks ✅ Use waitFor for async updates ✅ Prefill cache when testing with existing data ✅ Silence console errors in tests ❌ Don't test implementation details ❌ Don't mock TanStack Query internals