Testing basics
A test is code that runs part of the app and checks the result. In framework mode it helps that loader and action are plain functions: you can pass them a request and look at what they return. Components that need a router are tested with createRoutesStub — a small test router.
Installing
npm install -D vitest jsdom @testing-library/reactVitest is a test framework for Vite projects, jsdom imitates a browser, and Testing Library renders components. Add the script "test": "vitest" to package.json.
The reactRouter() plugin from vite.config.ts is not needed in tests, so create a separate config file:
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: { environment: "jsdom" },
});Testing a loader
Tests sit next to the code in *.test.ts files. Let's check that the post loader throws a 404 for an id that does not exist:
// app/routes/post.test.ts
import { describe, expect, it } from "vitest";
import { loader } from "./post";
import type { Route } from "./+types/post";
describe("post loader", () => {
it("returns 404 for a missing post", async () => {
const args = {
request: new Request("http://localhost/posts/999"),
params: { postId: "999" },
context: {},
} as Route.LoaderArgs;
const error = await loader(args).catch((e: unknown) => e);
expect(error).toBeInstanceOf(Response);
expect((error as Response).status).toBe(404);
});
});npm test runs the test, and Vitest reports in the terminal that it passed. The as Route.LoaderArgs cast is needed because real arguments have a few extra internal fields.
Testing a component
createRoutesStub builds a router from route descriptions. The loader in it can be replaced with a stub:
// app/routes/home.test.tsx
import { render, screen } from "@testing-library/react";
import { createRoutesStub } from "react-router";
import { expect, it } from "vitest";
import Home from "./home";
it("shows the list of posts", async () => {
const Stub = createRoutesStub([
{
path: "/",
Component: Home,
loader: () => ({ posts: [{ id: 1, title: "Hello, world", body: "" }] }),
},
]);
render(<Stub initialEntries={["/"]} />);
expect(await screen.findByText("Hello, world")).toBeTruthy();
});The test renders the home page with one post and finds its title. findByText waits until the stub loader finishes. If the component complains about its prop types, wrap it: Component: () => ... using the useLoaderData hook.
What to test
| What | With |
|---|---|
Form validation in an action | Vitest, calling the function |
Redirecting a guest in a loader | Vitest, checking the thrown response |
| Rendering data and errors in a component | createRoutesStub and Testing Library |
| The whole user journey in a browser | End-to-end tests, for example Playwright |
Common mistake
Testing a loader that works with the real blog.db database. The test adds or removes rows, and the result depends on what was in the database. For tests use a separate database (via the DATABASE_PATH environment variable) or replace the data module functions.