Disclaimer: I talk primarily about the code-based routing using React.
I recently rewrote an app to use a TanStack Router and naturally wondered how to test our routing.
We are on our own here 😮
To my surprise, the official docs don't mention testing at all. I found some mentions on GH but these are just indirect hints. That might be because they think there is nothing special about the test setup, but it still leaves people confused about best practices or just a basic setup.
So I took a quick stab at this challenge and figured out a couple things that will hopefully make it work for you.
Complete example
example.test.ts
import { createMemoryHistory, createRouter, RouterProvider } from '@tanstack/react-router';
import { render } from '@testing-library/react';
import { act } from 'react';
it('renders app', async () => {
const { app } = await setUp();
expect(app).toBeTruthy();
const heading = app.getByRole('heading', { name: 'Hello Router' });
expect(heading).toBeTruthy();
});
async function setUp() {
const router = createRouter({
// Very important to prevent test slowdown
defaultPendingMinMs: 0,
routeTree: rootRoute.addChildren([TEST_INDEX_ROUTE, ...realRoutes]),
});
const app = render(<RouterProvider<typeof router> router={router} />);
// Either you can make this part of setup, or call it in each test manually
await act(() =>
router.navigate({
search: { account_uuid: 'account_uuid' },
to: '/account',
}),
);
return {
router,
};
}
// Optional, if you want to prevent real components from loading initially
const TEST_INDEX_ROUTE = createRoute({
component: () => <div>Dummy index</div>,
getParentRoute: () => realRootRoute,
path: '/',
});
With a code snippet out of the way, let's talk details and some important principles.
Keep it real
You might have noticed that I am using real routes for both the root route and the routes under test. This is exactly what you want in order to keep your tests as close to the production environment as possible.
Ideally, I wouldn't need the TEST_INDEX_ROUTE
either and I'd just use the production routes, but I didn't want to navigate to a real "/" route and initialize components I didn't want to test. That's why I am using this dummy as a starting point.
In doing so, my testing router now differs from the production one. So I need to hint this fact to my router provider as it would expect the production router type by default and TS would produce a type mismatch error.
const router = createRouter({
defaultPendingMinMs: 0,
routeTree: rootRoute.addChildren([TEST_INDEX_ROUTE, ...realRoutes]),
});
// Let TS know we are using a different router
const app = render(<RouterProvider<typeof router> router={router} />);
The alternative way is to use the real routes but set the initial route to something else than /
using custom memory history:
const memoryHistory = createMemoryHistory({
initialEntries: ['/account?account_uuid=account-uuid'],
initialIndex: 0,
});
const router = createRouter({
defaultPendingMinMs: 0,
history: memoryHistory,
routeTree: rootRoute.addChildren(refundRoutes),
});
Not bad, but I don't like that the initial URL has to be specified as a URI string without any type guarantees. On the other hand, calling navigate
explicitly gives you full control and type safety. Just don't forget to wrap it in act
since navigation often changes React state and you'd get warnings and perhaps wrong state if you didn't.
Beware the pending state
Maybe you wondered what this setting is about:
const router = createRouter({
// Very important to prevent test slowdown
defaultPendingMinMs: 0,
The default value is 500ms and it's the time during which the route is in a pending (not displayed) state. I gathered that it should make people navigating between pages aware that the page changed. This to me sounds like a hard to justify slow down and I always recommend setting it to 0 in production code.
But it's easy to forget that you need the same setting in tests as well, otherwise it can prolong each of your test cases by 500ms 🙈.
Performance
I found that tests involving Tanstack Router and full route rendering tend to be pretty slow - couple 100s ms. I'd recommend writing such tests only for the complex parts of your navigation and perhaps use shallow rendering to improve performance if absolutely necessary.
The good news is that lazy routes are respected in that they are not loaded until used. That isn't really surprising since the lazy loading, using the dynamic import()
is pure JS and nothing specific to Tanstack Router.
Final thoughts
As you see, there is not much test specific set up needed, and it's indeed the best strategy to change as little as possible for the sake of tests.
Let me know if you found a better way to set the tests up with Tanstack Router, especially regarding the performance. Cheers.
Top comments (0)