Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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/react

Vitest 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

WhatWith
Form validation in an actionVitest, calling the function
Redirecting a guest in a loaderVitest, checking the thrown response
Rendering data and errors in a componentcreateRoutesStub and Testing Library
The whole user journey in a browserEnd-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.

Thanks for reading!

Donate to the site — we work hard for you.