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.

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.
Start by installing the test tooling:
npm i -D vitest @testing-library/react @testing-library/jest-dom jsdomvitest 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.
Query hooks are tested by wrapping them in a provider that creates a fresh QueryClient per test. This keeps the cache isolated between tests:
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.
The router is tested by creating a router instance from the same route tree as production, then rendering the RouterProvider:
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.
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:
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.
For integration tests, mock the entire backend with MSW. Query functions use regular fetch; MSW intercepts it and returns fake data:
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.
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:
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!