UNPKG

5.02 kB Markdown View Raw
1---
2title: Testing
3order: 9
4---
5
6# Testing
7
8[MODES: framework, data]
9
10## Introduction
11
12When components use things like `useLoaderData`, `<Link>`, etc, they are required to be rendered in context of a React Router app. The `createRoutesStub` function creates that context to test components in isolation.
13
14Consider a login form component that relies on `useActionData`
15
16```tsx
17import { useActionData } from "react-router";
18
19export function LoginForm() {
20 const actionData = useActionData();
21 const errors = actionData?.errors;
22 return (
23 <Form method="post">
24 <label>
25 <input type="text" name="username" />
26 {errors?.username && <div>{errors.username}</div>}
27 </label>
28
29 <label>
30 <input type="password" name="password" />
31 {errors?.password && <div>{errors.password}</div>}
32 </label>
33
34 <button type="submit">Login</button>
35 </Form>
36 );
37}
38```
39
40We can test this component with `createRoutesStub`. It takes an array of objects that resemble route modules with loaders, actions, and components.
41
42```tsx
43import { createRoutesStub } from "react-router";
44import {
45 render,
46 screen,
47 waitFor,
48} from "@testing-library/react";
49import userEvent from "@testing-library/user-event";
50import { LoginForm } from "./LoginForm";
51
52test("LoginForm renders error messages", async () => {
53 const USER_MESSAGE = "Username is required";
54 const PASSWORD_MESSAGE = "Password is required";
55
56 const Stub = createRoutesStub([
57 {
58 path: "/login",
59 Component: LoginForm,
60 action() {
61 return {
62 errors: {
63 username: USER_MESSAGE,
64 password: PASSWORD_MESSAGE,
65 },
66 };
67 },
68 },
69 ]);
70
71 // render the app stub at "/login"
72 render(<Stub initialEntries={["/login"]} />);
73
74 // simulate interactions
75 userEvent.click(screen.getByText("Login"));
76 await waitFor(() => screen.findByText(USER_MESSAGE));
77 await waitFor(() => screen.findByText(PASSWORD_MESSAGE));
78});
79```
80
81## Using with Framework Mode Types
82
83It's important to note that `createRoutesStub` is designed for _unit_ testing of reusable components in your application that rely on contextual router information (i.e., `loaderData`, `actionData`, `matches`). These components usually obtain this information via the hooks (`useLoaderData`, `useActionData`, `useMatches`) or via props passed down from the ancestor route component. We **strongly** recommend limiting your usage of `createRoutesStub` to unit testing of these types of reusable components.
84
85`createRoutesStub` is _not designed_ for (and is arguably incompatible with) direct testing of Route components using the [`Route.\*`](../../explanation/type-safety) types available in Framework Mode. This is because the `Route.*` types are derived from your actual application - including the real `loader`/`action` functions as well as the structure of your route tree structure (which defines the `matches` type). When you use `createRoutesStub`, you are providing stubbed values for `loaderData`, `actionData`, and even your `matches` based on the route tree you pass to `createRoutesStub`. Therefore, the types won't align with the `Route.*` types and you'll get type issues trying to use a route component in a route stub.
86
87```tsx filename=routes/login.tsx
88export default function Login({
89 actionData,
90}: Route.ComponentProps) {
91 return <Form method="post">...</Form>;
92}
93```
94
95```tsx filename=routes/login.test.tsx
96import LoginRoute from "./login";
97
98test("LoginRoute renders error messages", async () => {
99 const Stub = createRoutesStub([
100 {
101 path: "/login",
102 Component: LoginRoute,
103 // ^ ❌ Types of property 'matches' are incompatible.
104 action() {
105 /*...*/
106 },
107 },
108 ]);
109
110 // ...
111});
112```
113
114These type errors are generally accurate if you try to setup your tests like this. As long as your stubbed `loader`/`action` functions match your real implementations, then the types for `loaderData`/`actionData` will be correct, but if they differ your types will be lying to you.
115
116`matches` is more complicated since you don't usually stub out all of the ancestor routes. In this example, there is no `root` route so `matches` will only contain your test route, while it will contain the root route and any other ancestors at runtime. There's no great way to automatically align the typegen types with the runtime types in your test.
117
118Therefore, if you need to test Route level components, we recommend you do that via an Integration/E2E test (Playwright, Cypress, etc.) against a running application because you're venturing out of unit testing territory when testing your route as a whole.
119
120If you _need_ to write a unit test against the route, you can add a `@ts-expect-error` comment in your test to silence the TypeScript error:
121
122```tsx
123const Stub = createRoutesStub([
124 {
125 path: "/login",
126 // @ts-expect-error: `matches` won't align between test code and app code
127 Component: LoginRoute,
128 action() {
129 /*...*/
130 },
131 },
132]);
133```