Files
sase.tr/.agents/skills/tanstack-query/references/testing.md
Sase Dev 996d614d50 feat: admin user creation, Vite migration, dialog fix, pl24 integration
- Add POST /admin/users endpoint with password hashing and role support
- Add user creation dialog to admin users page
- Migrate web from Next.js to Vite + TanStack Router
- Fix Dialog component positioning for Tailwind CSS v4
- Add @source directive for @sase/ui package scanning
- Add pl24 integration parsers and vehicle decode flow
- Backup old Next.js app to apps/web-nj

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 14:24:58 +00:00

6.1 KiB

Testing TanStack Query

Testing queries, mutations, and components


Setup

npm install -D @testing-library/react @testing-library/jest-dom vitest msw

Test Utils

// 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(
    <QueryClientProvider client={testQueryClient}>
      {ui}
    </QueryClientProvider>
  )
}

Testing Queries

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

describe('useTodos', () => {
  it('fetches todos successfully', async () => {
    const { result } = renderHook(() => useTodos(), {
      wrapper: ({ children }) => (
        <QueryClientProvider client={createTestQueryClient()}>
          {children}
        </QueryClientProvider>
      ),
    })

    // 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

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

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

import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { TodoList } from './TodoList'

test('displays todos', async () => {
  renderWithClient(<TodoList />)

  expect(screen.getByText(/loading/i)).toBeInTheDocument()

  await waitFor(() => {
    expect(screen.getByText('Test todo')).toBeInTheDocument()
  })
})

test('adds new todo', async () => {
  renderWithClient(<TodoList />)

  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

test('uses prefilled cache', () => {
  const queryClient = createTestQueryClient()

  // Prefill cache
  queryClient.setQueryData(['todos'], [
    { id: 1, title: 'Cached todo', completed: false },
  ])

  render(
    <QueryClientProvider client={queryClient}>
      <TodoList />
    </QueryClientProvider>
  )

  // Should immediately show cached data
  expect(screen.getByText('Cached todo')).toBeInTheDocument()
})

Testing Optimistic Updates

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 }) => (
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    ),
  })

  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