| | |

TanStack Router 14 ⚛️ Testing — Testing routes and components

⏱️ Estimated read: 9 min | |

The previous chapters covered the route configuration, the loaders, the actions, and the error boundaries. This chapter covers the testing: the verification that the routes render the correct components for the correct URLs, that the loaders fetch the correct data, that the actions mutate the state, and that the error boundaries catch the failures. The LFCS exam does not test TanStack Router, but the front-end engineering certification paths do. The critical distinction is between the full application test (the router with the real route tree) and the component test (the individual component in isolation). The full test uses createMemoryHistory and createRouter to simulate the navigation. The component test uses createRoute with the getParentRoute mock. The exam tests the memory history and the router harness.

Key point: The testing requires createMemoryHistory for the navigation control and createRouter with the route tree for the real routing behavior. The RouterProvider renders the router in the test. The router.load() awaits the initial navigation and the loaders. The screen.findByText waits for the async data. The @testing-library/react provides the render, screen, and userEvent. The @testing-library/jest-dom provides the toBeInTheDocument and the other matchers. The msw (Mock Service Worker) mocks the network requests at the fetch level.


Why testing requires a specific mental model

The memory history problem. The browser history is not available in the test environment (there is no real URL). The createMemoryHistory({ initialEntries: ['/posts'] }) creates the in-memory history with the initial entries. The test navigates by calling router.navigate({ to: '/posts/1' }), and the router updates the in-memory history. The initialEntries sets the starting URL, which is the same as the user opening the URL in the browser .

The async loader problem. The loader fetches the data asynchronously. The router.load() returns a Promise that resolves when the loader completes. The test awaits it before the assertion. The RouterProvider renders the loading state if the data is not yet available, and the screen.findByText waits for the element to appear. The combination of the router.load() and the findByText handles both the router-level and the component-level async behavior.

The route tree problem. The router must be created with the real route tree, not with the mock route. The createRouter({ routeTree, history }) uses the generated route tree. The test does not mock the routes; it uses the actual ones. The benefit is that the test verifies the real route configuration, the real loaders, and the real components. The cost is that the test is slower and depends on the entire route tree.

The component test problem. The individual component test is the alternative. The component can be rendered inside the RouterProvider with the minimal route tree: the createRootRoute, the createRoute, and the createRouter. The component is the component of the route. The test isolates the component from the rest of the application.

The network mock problem. The loader fetches the data from the API. The test must not hit the real API. The msw intercepts the fetch at the network level. The setupServer with the http.get('/api/posts', () => HttpResponse.json(mockPosts)) returns the mock data. The handler is the mock; the loader fetches the mock data. The test is deterministic and does not depend on the external service.

The trade-off. The full application test is the most realistic but the slowest. The component test is the fastest but less realistic. The msw is the correct network mock because it works at the fetch level, which matches the loader’s fetch call.


a. The test setup and the memory history

The test uses the createMemoryHistory and the createRouter with the real route tree.

// src/test/setup.ts (or the test file)
import { createMemoryHistory, createRouter } from '@tanstack/react-router'
import { routeTree } from '../routeTree.gen'

function renderApp(initialEntries: string[]) {
  const history = createMemoryHistory({ initialEntries })
  const router = createRouter({ routeTree, history })
  render(<RouterProvider router={router} />)
  return { router }
}

The createMemoryHistory({ initialEntries: ['/posts'] }) sets the initial URL. The createRouter({ routeTree, history }) creates the router with the real route tree and the in-memory history. The RouterProvider renders the router.

The test awaits the initial navigation with the router.load().

import { render, screen } from '@testing-library/react'
import { RouterProvider } from '@tanstack/react-router'

test('renders the posts list', async () => {
  const { router } = renderApp(['/posts'])
  await router.load()
  expect(screen.getByText('Hello, posts')).toBeInTheDocument()
})

The await router.load() waits for the initial loaders. The screen.getByText finds the rendered element. The toBeInTheDocument is the jest-dom matcher .


b. The network mock with MSW

The msw mocks the network requests. The loader fetches the mock data.

// src/test/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/posts', () => {
    return HttpResponse.json([
      { id: '1', title: 'First post' },
      { id: '2', title: 'Second post' },
    ])
  }),
]

The http.get('/api/posts', ...) intercepts the fetch. The HttpResponse.json returns the mock data. The loader receives the mock data, not the real API response .

The test server is set up in the test file.

import { setupServer } from 'msw/node'
import { handlers } from './mocks/handlers'

const server = setupServer(...handlers)

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

test('renders the posts', async () => {
  const { router } = renderApp(['/posts'])
  await router.load()
  expect(await screen.findByText('First post')).toBeInTheDocument()
  expect(await screen.findByText('Second post')).toBeInTheDocument()
})

The server.listen() starts the interceptor. The server.resetHandlers() resets the handlers between the tests. The server.close() stops the interceptor after the test suite. The findByText is the async version of the getByText, which waits for the element to appear .


c. The component test with the minimal route

The individual component can be tested with the minimal route tree.

import { createRootRoute, createRoute, createRouter, RouterProvider } from '@tanstack/react-router'
import { render, screen } from '@testing-library/react'

function PostComponent() {
  return <h1>Post</h1>
}

test('renders the post component', async () => {
  const rootRoute = createRootRoute()
  const postRoute = createRoute({
    getParentRoute: () => rootRoute,
    path: '/posts/$postId',
    component: PostComponent,
  })
  const routeTree = rootRoute.addChildren([postRoute])
  const router = createRouter({ routeTree })

  render(<RouterProvider router={router} />)
  await router.load()

  expect(screen.getByText('Post')).toBeInTheDocument()
})

The createRootRoute() creates the root. The createRoute({ getParentRoute: () => rootRoute, path: '/posts/$postId', component: PostComponent }) creates the route. The routeTree is the root with the post as the child. The createRouter({ routeTree }) creates the router. The RouterProvider renders the component. The router.load() awaits the initial load.

The component test isolates the component from the rest of the application. The test does not depend on the generated route tree.


Complete Example Session

# ============================================
# PART 1: TEST HARNESS
# ============================================

function renderApp(initialEntries) {
  const history = createMemoryHistory({ initialEntries })
  const router = createRouter({ routeTree, history })
  render(<RouterProvider router={router} />)
  return { router }
}
# ============================================
# PART 2: MEMORY HISTORY
# ============================================

const history = createMemoryHistory({ initialEntries: ['/posts'] })
# ============================================
# PART 3: ROUTER
# ============================================

const router = createRouter({ routeTree, history })
# ============================================
# PART 4: AWAIT LOAD
# ============================================

await router.load()
# ============================================
# PART 5: MSW HANDLER
# ============================================

http.get('/api/posts', () => HttpResponse.json(mockPosts))
# ============================================
# PART 6: MSW SERVER
# ============================================

const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterAll(() => server.close())
# ============================================
# PART 7: ASYNC ASSERTION
# ============================================

expect(await screen.findByText('First post')).toBeInTheDocument()
# ============================================
# PART 8: COMPONENT TEST
# ============================================

const rootRoute = createRootRoute()
const postRoute = createRoute({ getParentRoute: () => rootRoute, path: '/', component: PostComponent })
const routeTree = rootRoute.addChildren([postRoute])
const router = createRouter({ routeTree })
# ============================================
# PART 9: VERIFY
# ============================================

# renderApp(['/posts']) → await load → posts render
# Network mocked by MSW
# Component isolated with the minimal route

The nine parts trace the testing: harness, memory history, router, await load, MSW handler, MSW server, async assertion, component test, verification.


Quick Reference

Test Setup

ToolPurpose
createMemoryHistoryIn-memory URL
createRouterRouter with the route tree
RouterProviderRender the router
router.load()Await the initial navigation

Async Assertions

ToolPurpose
findByTextWait for the element
findByRoleWait by role
waitForWait for the arbitrary condition

MSW

FunctionPurpose
http.getMock the GET
HttpResponse.jsonReturn the JSON
setupServerServer with the handlers
server.listen()Start
server.close()Stop

Component Test

FunctionPurpose
createRootRouteRoot route
createRouteChild route
addChildrenAttach to the parent

Best Practices

✅ Do This:

const history = createMemoryHistory({ initialEntries: ['/posts'] })
<!-- Use the memory history for the test -->         # ✅
await router.load()
<!-- Await the initial navigation -->                # ✅
expect(await screen.findByText('...')).toBeInTheDocument()
<!-- Use the async assertion -->                     # ✅

❌ Don’t Do This:

# Use the browser history
<!-- Not available in the test environment -->       # ❌
# Skip the router.load()
<!-- The loader may not finish -->                   # ❌
# Hit the real API
<!-- Use MSW for the deterministic mock -->          # ❌

Common Pitfalls

PitfallWhy It HappensFix
Test hangsNo router.load()Await the load
Element not foundNo async assertionUse findByText
Network errorReal API in the testUse MSW
Wrong URLWrong initialEntriesCheck the initial entry
Component isolated failsMissing route contextUse createRootRoute + createRoute

Real-World Examples

1. Memory History

createMemoryHistory({ initialEntries: ['/posts'] })

2. Router

createRouter({ routeTree, history })

3. Await Load

await router.load()

4. MSW Handler

http.get('/api/posts', () => HttpResponse.json(mockData))

5. MSW Server

const server = setupServer(...handlers)

6. Async Assert

await screen.findByText('Post 1')

7. Component Test

const rootRoute = createRootRoute()
const route = createRoute({ getParentRoute: () => rootRoute, path: '/' })

8. Attach Children

const tree = rootRoute.addChildren([route])

9. Render Router

render(<RouterProvider router={router} />)

10. jest-dom Matchers

expect(el).toBeInTheDocument()

Visual

Test Flow

┌──────────────────────────────────────────────┐
│  createMemoryHistory(['/posts'])             │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  createRouter({ routeTree, history })        │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  render(<RouterProvider router={router} />)  │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  await router.load()                         │
│  → Loader fetches the data (mocked by MSW)   │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  await screen.findByText('Post 1')           │
│  → Assert the rendered content               │
└──────────────────────────────────────────────┘

Test vs Production

┌──────────────────────────────────────────────┐
│  Production                                  │
│  ├── createBrowserHistory                    │
│  ├── Real API                                │
│  └── Browser URL                             │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Test                                        │
│  ├── createMemoryHistory                     │
│  ├── MSW mock                                │
│  └── In-memory URL                           │
└──────────────────────────────────────────────┘

Summary

ItemValue
HistorycreateMemoryHistory
RoutercreateRouter({ routeTree, history })
Render<RouterProvider />
Awaitrouter.load()
Async assertfindByText
Network mockMSW setupServer
Component testcreateRootRoute + createRoute
Matchers@testing-library/jest-dom

Key takeaways:

  • The memory history is the test navigation. The createMemoryHistory({ initialEntries: ['/posts'] }) sets the initial URL. The test navigates by calling the router methods, and the router updates the in-memory history.
  • The router.load() awaits the initial navigation. The loaders are asynchronous. The test must await the router.load() before the assertions. The findByText handles the component-level async rendering.
  • The MSW is the network mock. The http.get intercepts the fetch at the network level. The loader receives the mock data. The test is deterministic and does not depend on the external service.
  • The component test uses the minimal route. The createRootRoute and the createRoute create the minimal tree. The component is the route’s component. The test isolates the component from the application.
  • The @testing-library/jest-dom provides the matchers. The toBeInTheDocument and the other matchers are the assertions. The @testing-library/react provides the render and the screen.

Remember: The memory history is the test. The router.load() is the await. The MSW is the mock. The findByText is the async. The createRootRoute is the isolation. The toBeInTheDocument is the assertion. The exam tests the test harness and the async patterns.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!