Learn TanStack - Testing & Quality Assurance
Episode 16 of 24

Learn TanStack - Testing & Quality Assurance

This episode builds testing habits: unit testing Query hooks with renderHook, testing Router navigation, testing table and virtual list rendering, and integration testing with a mock server using MSW.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

A feature that works in development can fail completely in production if it isn't tested. TanStack has one big advantage for testing: because the libraries are headless and stateful, query, navigation, and table logic can be tested without depending on a real server.

Episode 16 covers unit testing Query hooks, testing Router navigation, testing table and virtual list rendering, and integration testing with a mock server using MSW.

The goal of this episode: you'll have a fast, deterministic test suite that runs in CI — exactly what we need for the pipeline in episode 19.

Test Tooling Setup

Vitest, Testing Library, and jsdom

Start by installing the test tooling:

Install test tooling
npm i -D vitest @testing-library/react @testing-library/jest-dom jsdom

vitest is a fast test runner integrated with Vite. @testing-library/react provides render and renderHook, jest-dom adds DOM matchers, and jsdom simulates a browser environment in Node.js.

Unit Testing TanStack Query Hooks

renderHook with a QueryClientProvider Wrapper

Query hooks are tested by wrapping them in a provider that creates a fresh QueryClient per test. This keeps the cache isolated between tests:

JSUnit test hook useQuery
import { renderHook, waitFor } from "@testing-library/react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
 
function wrapper({ children }) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  })
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
 
test("memuat data pengguna", async () => {
  const { result } = renderHook(
    () => useQuery({
      queryKey: ["pengguna"],
      queryFn: async () => ({ nama: "Arman" }),
    }),
    { wrapper }
  )
 
  await waitFor(() => expect(result.current.isSuccess).toBe(true))
  expect(result.current.data.nama).toBe("Arman")
})

retry: false in the default options ensures tests don't wait through retries when a query fails — failures surface fast. renderHook with a wrapper tests the query exactly as it runs in the app.

Testing TanStack Router Navigation

Rendering the Router Provider Directly

The router is tested by creating a router instance from the same route tree as production, then rendering the RouterProvider:

JSTest navigasi router
import { render, screen } from "@testing-library/react"
import { RouterProvider, createRouter } from "@tanstack/react-router"
import { routeTree } from "./routeTree"
 
const router = createRouter({ routeTree })
 
test("menampilkan halaman utama", async () => {
  render(<RouterProvider router={router} />)
  expect(await screen.findByText("Halaman utama")).toBeInTheDocument()
})

createRouter({ routeTree }) uses the exact same tree as the app. With findByText, the test waits for the loader to finish and the page to render — simultaneously verifying that navigation and loaders work end to end.

Testing Table Rendering and Virtualized Lists

Checking Which Rows Are Actually Rendered

Tables and virtual lists only render a subset of rows. Tests should assert that visible rows appear and items outside the viewport aren't rendered — that's the virtualization behavior itself:

JSTest render baris virtual
test("merender sebagian baris saja", () => {
  const { container } = render(<DaftarVirtual count={1000} />)
  const baris = container.querySelectorAll("div.baris")
  expect(baris.length).toBeLessThan(50)
})

With 1000 items, expect far fewer row DOM nodes than 1000. toBeLessThan(50) proves virtualization is active. For table state, test through interaction: click a header to sort, click a page to paginate, then check the rows that appear.

Integration Testing with a Mock Server

MSW for Backend Simulation

For integration tests, mock the entire backend with MSW. Query functions use regular fetch; MSW intercepts it and returns fake data:

JSMock server dengan MSW
import { setupServer } from "msw/node"
import { http, HttpResponse } from "msw"
 
const server = setupServer(
  http.get("/api/todos", () =>
    HttpResponse.json([{ id: 1, judul: "Belajar TanStack" }])
  )
)
 
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

setupServer simulates an HTTP server in Node.js. Tests can then render full components, run mutations, and verify query invalidation — all without a real network. resetHandlers keeps scenarios isolated between tests.

Tip

In tests, use fake timers and advanceTimersByTime to test refetchInterval and retryDelay without waiting real time. This keeps the test suite fast.

Conclusion

Episode 16 wrapped up quality assurance: Query hooks tested with renderHook and a wrapper, Router navigation tested through RouterProvider, tables and virtual lists tested by counting DOM nodes, and entire flows tested end to end with MSW as the mock server.

Key takeaways:

  • A fresh QueryClient per test keeps the cache isolated.
  • retry: false speeds up tests that expect failures.
  • The router is tested with createRouter from the production route tree.
  • Test virtualization by counting the rendered rows.
  • MSW mimics a backend without a real network.
  • Fake timers test polling and retries quickly.

In the next episode, episode 17, we'll discuss TypeScript and schema safety — strong typing in Query and Table, inference patterns and utility types, type-safe data loading in Router, and schema validation with Zod. All your code is about to become much safer!

Learn TanStack - Testing & Quality Assurance | Learn TanStack