React Router 测试指南:使用 createRoutesStub 隔离测试依赖路由上下文的组件
【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router
在 React Router 应用中,像useLoaderData、useActionData、<Link>、useMatches这类 API 要求组件必须被渲染在 React Router 的应用上下文(router context)之内,否则会直接报错。createRoutesStub正是为此设计的测试工具:它把一组"长得像路由模块"的对象转换成一个完整可渲染的 React 组件,为你的组件提供加载器、动作函数与路由匹配数据等上下文。读完本文,你将掌握如何用createRoutesStub对可复用组件做单元测试、哪些能力受其支持、为何它不适用于 Framework Mode 下带类型推断的 Route 组件,以及在需要整路由测试时应该转向哪类测试手段。
为什么需要createRoutesStub
当你的组件内部直接使用了路由上下文 API 时,脱离路由器它就无法运行。例如下面的登录表单组件,它通过useActionData()读取表单提交后 action 返回的数据并渲染校验错误:
import { useActionData } from "react-router"; export function LoginForm() { const actionData = useActionData(); const errors = actionData?.errors; return ( <Form method="post"> <label> <input type="text" name="username" /> {errors?.username && <div>{errors.username}</div>} </label> <label> <input type="password" name="password" /> {errors?.password && <div>{errors.password}</div>} </label> <button type="submit">Login</button> </Form> ); }在测试里如果直接render(<LoginForm />),由于既没有actionData、也没有Form所需的上下文,测试将无法运行或无法断言错误文案。createRoutesStub会在内存中替你搭一个最小可用的路由环境,从而在"无服务器、无完整应用"的情况下单独测试这类组件。
API 形态与返回组件的 Props
createRoutesStub从react-router导出(导出位置),其函数签名(见 createRoutesStub API 文档)为:
function createRoutesStub( routes: StubRouteObject[], _context?: RouterContextProvider, )- routes:一组"仿真路由模块对象",字段仿照路由模块导出(
loader、action、Component、HydrateFallback、ErrorBoundary、children、meta、links、middleware等),来源见 StubRouteExtensions 接口; - _context:可选的
RouterContextProvider,用于向路由 middleware / loader / action 注入应用上下文值; - 返回值:一个可渲染的 React 组件(即测试桩组件)。
测试桩组件本身还接收以下可选的 props(定义见 RoutesTestStubProps):
| Prop | 作用 |
|---|---|
initialEntries | 初始历史记录条目,可放入多个 URL 以模拟"从某页再导航到某页"(如测试返回导航);缺省时渲染initialEntries的最后一项 |
initialIndex | 指定从历史栈的哪一项开始渲染,默认是initialEntries的最后一项 |
hydrationData | 为路由预置初始 loader / action 数据,例如{ loaderData: { "/contact": { locale: "en-US" } } } |
future | 模拟 react-router.config.ts 中的未来功能开关(future flags) |
从底层实现看(createRoutesStub 实现),stub 组件会在首次渲染时把传入的路由经过processRoutes处理,补全 manifest 与 routeModules,再通过createMemoryRouter创建内存路由器,最终用FrameworkContext.Provider包裹RouterProvider输出——这意味着它在测试环境里复现了真实路由器的数据流,而非单纯的 Mock。
完整示例:用桩路由测试表单错误渲染
createRoutesStub接收的对象数组与路由模块非常相似——每个对象都带path,还可以带Component、loader、action。针对上面的LoginForm,可以构造一个"/login"路由桩,它的action直接返回两段错误信息,然后渲染桩组件并模拟点击提交:
import { createRoutesStub } from "react-router"; import { render, screen, waitFor, } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { LoginForm } from "./LoginForm"; test("LoginForm renders error messages", async () => { const USER_MESSAGE = "Username is required"; const PASSWORD_MESSAGE = "Password is required"; const Stub = createRoutesStub([ { path: "/login", Component: LoginForm, action() { return { errors: { username: USER_MESSAGE, password: PASSWORD_MESSAGE, }, }; }, }, ]); // render the app stub at "/login" render(<Stub initialEntries={["/login"]} />); // simulate interactions userEvent.click(screen.getByText("Login")); await waitFor(() => screen.findByText(USER_MESSAGE)); await waitFor(() => screen.findByText(PASSWORD_MESSAGE)); });要点拆解:
Component指向被测的LoginForm,测试桩会把它置于路由上下文内渲染,因此useActionData能读到 action 的返回值;- action 中的
errors键与Form提交路径匹配,模拟真实校验失败的返回; initialEntries={["/login"]}让内存路由初始就落在目标路径。
桩路由能覆盖的能力边界
从仓库的测试用例(stub-test.tsx)可以确认,createRoutesStub支持的验证范围相当广:
- 嵌套路由与 Outlet:父路由组件渲染
<Outlet />,子路由按path匹配渲染(见测试 "renders a nested route"); - loader 数据读取:既可以
useLoaderData读取,也可以从组件 props 接收loaderData(如Component({ loaderData })); - action 与表单提交:
<Form method="post">配合useActionData/ props 中的actionData; - 错误处理:
Component中抛错会被同路由ErrorBoundary捕获,useRouteError/errorprop 均可用; - fetcher:
useFetcher可以加载桩中的其他路由 loader(如/api返回自增计数); - middleware 与 context:middleware 可抛
redirect完成跳转,也可通过RouterContextProvider(或直接传普通对象)为loader的context注入自定义值; - meta / links:
meta与links函数同样作为桩路由字段被支持,仓库的 meta 测试 与 links 测试 均基于createRoutesStub编写。
Framework Mode 下的类型注意点
createRoutesStub被设计为面向可复用组件的单元测试——那些靠 hook 或父级 props 获取loaderData/actionData/matches的组件。仓库强烈建议把它的使用范围限定在这一类单元测试上。
需要特别强调的是,它并非为直接测试使用Route.*类型的路由组件而设计,两者甚至是基本互斥的。原因在于 Framework Mode 下的Route.*类型(类型安全说明)是从你的真实应用推导出来的:包括真实的loader/action函数,以及真实路由树结构(它决定了matches的类型)。而当你用createRoutesStub时,你提供的是桩化的loaderData、actionData与matches(由你传给 stub 的路由树决定),因此这些类型与Route.*类型不可能对齐。
例如一个使用Route.ComponentProps的路由模块:
export default function Login({ actionData, }: Route.ComponentProps) { return <Form method="post">...</Form>; }把它直接塞进桩路由会得到类型报错:
import LoginRoute from "./login"; test("LoginRoute renders error messages", async () => { const Stub = createRoutesStub([ { path: "/login", Component: LoginRoute, // ^ ❌ Types of property 'matches' are incompatible. action() { /*...*/ }, }, ]); // ... });为什么matches会不兼容?因为测试里通常不会把全部祖先路由都桩化出来——上面这个例子没有root路由,所以测试中的matches只包含被测试的这一条路由,而运行时matches还会包含 root 路由以及所有其他祖先路由。虽然只要桩里的loader/action与真实实现一致,loaderData/actionData的类型依然正确,但一旦二者不一致,类型就会"骗你";而对matches而言,几乎没有办法让 typegen 生成的类型与测试运行时的类型自动对齐。
什么场景该用别的测试方式
如果确实需要测试整条 Route(包括父级布局、真实的 matches 结构),那就已经超出了单元测试的范畴。仓库推荐改用Integration / E2E 测试(Playwright、Cypress 等)对着运行中的应用来验证——这种测试能还原完整路由树、真实 loader 执行链与页面间的导航。这也与桩实现源码中的注释保持一致:测试路由时应 stub 掉 loader/action/middleware,而不是试图重建完整的 loader/SSR/hydration 流程,后者更适合交给 E2E 测试(见 routes-test-stub.tsx 的说明)。React Router 仓库自身的集成测试即采用 Playwright 编写并置于 integration 目录,可供参考。
如果不得已要对路由写单元测试,一个务实的做法是在桩路由对象里用@ts-expect-error屏蔽该处类型错误(注意保证你的桩action/loader与真实实现一致,避免类型失真):
const Stub = createRoutesStub([ { path: "/login", // @ts-expect-error: `matches` won't align between test code and app code Component: LoginRoute, action() { /*...*/ }, }, ]);模式适用范围与小结
本文档标注[MODES: framework, data],即createRoutesStub在 Data 模式(纯数据路由,无 Vite 插件)与 Framework 模式下均可使用;Data 模式下的指引直接复用本篇(见 Data 模式测试入口)。
实践小结:
- 单元测试可复用组件:优先用
createRoutesStub,构造迷你路由树并 stub 掉loader/action; - 验证与真实路由结构无关的能力:嵌套 Outlet、ErrorBoundary、fetcher、middleware context 等都可在桩内验证;
- 避开
Route.*类型路由的单元测试:typegen 的强类型无法与桩路由的运行时结构自动对齐,需要整路由语义时应转向对运行中应用的 Integration/E2E 测试; - 不得已时的逃生舱:保持桩函数与真实实现一致的前提下,用
@ts-expect-error仅屏蔽matches不匹配这一条。
如需配置完整的路由模块类型安全(生成+types与rootDirs设置),可参考 路由模块类型安全指南;createRoutesStub的权威 API 说明见 API 参考文档。
【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考