news 2026/9/8 19:18:14

React Router 测试指南:使用 createRoutesStub 隔离测试依赖路由上下文的组件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React Router 测试指南:使用 createRoutesStub 隔离测试依赖路由上下文的组件

React Router 测试指南:使用 createRoutesStub 隔离测试依赖路由上下文的组件

【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router

在 React Router 应用中,像useLoaderDatauseActionData<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

createRoutesStubreact-router导出(导出位置),其函数签名(见 createRoutesStub API 文档)为:

function createRoutesStub( routes: StubRouteObject[], _context?: RouterContextProvider, )
  • routes:一组"仿真路由模块对象",字段仿照路由模块导出(loaderactionComponentHydrateFallbackErrorBoundarychildrenmetalinksmiddleware等),来源见 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,还可以带Componentloaderaction。针对上面的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 均可用;
  • fetcheruseFetcher可以加载桩中的其他路由 loader(如/api返回自增计数);
  • middleware 与 context:middleware 可抛redirect完成跳转,也可通过RouterContextProvider(或直接传普通对象)为loadercontext注入自定义值;
  • meta / linksmetalinks函数同样作为桩路由字段被支持,仓库的 meta 测试 与 links 测试 均基于createRoutesStub编写。

Framework Mode 下的类型注意点

createRoutesStub被设计为面向可复用组件的单元测试——那些靠 hook 或父级 props 获取loaderData/actionData/matches的组件。仓库强烈建议把它的使用范围限定在这一类单元测试上。

需要特别强调的是,它并非为直接测试使用Route.*类型的路由组件而设计,两者甚至是基本互斥的。原因在于 Framework Mode 下的Route.*类型(类型安全说明)是从你的真实应用推导出来的:包括真实的loader/action函数,以及真实路由树结构(它决定了matches的类型)。而当你用createRoutesStub时,你提供的是桩化的loaderDataactionDatamatches(由你传给 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 模式测试入口)。

实践小结:

  1. 单元测试可复用组件:优先用createRoutesStub,构造迷你路由树并 stub 掉loader/action
  2. 验证与真实路由结构无关的能力:嵌套 Outlet、ErrorBoundary、fetcher、middleware context 等都可在桩内验证;
  3. 避开Route.*类型路由的单元测试:typegen 的强类型无法与桩路由的运行时结构自动对齐,需要整路由语义时应转向对运行中应用的 Integration/E2E 测试;
  4. 不得已时的逃生舱:保持桩函数与真实实现一致的前提下,用@ts-expect-error仅屏蔽matches不匹配这一条。

如需配置完整的路由模块类型安全(生成+typesrootDirs设置),可参考 路由模块类型安全指南;createRoutesStub的权威 API 说明见 API 参考文档。

【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 19:17:45

豆包+FPGA开发:从Vivado报错到Verilog代码生成的AI实战指南

这段时间我一直在用豆包帮我干活&#xff0c;别的不说&#xff0c;文档问答、代码补充是真的方便。不过大家讨论得多的还是Linux命令、Python脚本&#xff0c;今天我想聊一个相对冷门的组合&#xff1a;让豆包参与FPGA开发。FPGA工具链里绕不开的就是Vivado&#xff0c;我从201…

作者头像 李华
网站建设 2026/9/8 19:17:45

开源终端AI编程助手OpenCode:安装配置与实战指南

最近这段时间&#xff0c;我的终端里几乎每天都开着opencode&#xff0c;身边不少同事也被我拉到这条路上来了。如果你已经刷到过这个热搜词&#xff0c;可能和我最开始一样有一堆疑问&#xff1a;它是不是某家公司出的商业工具&#xff1f;和 Claude Code 到底能不能比&#x…

作者头像 李华
网站建设 2026/9/8 19:16:51

Android出海系列-VTS测试介绍

一、什么是 VTS&#xff0c;为什么它对出海至关重要 从 Android 8.0 开始&#xff0c;Google 引入 Project Treble&#xff0c;将系统框架层与厂商实现层&#xff08;Vendor&#xff09;解耦。Treble 之前&#xff0c;每次系统升级都需要厂商同步修改底层实现&#xff1b;Trebl…

作者头像 李华
网站建设 2026/9/8 19:16:01

OpenClaw 2.0开源数字员工实测:从聊天机器人到本地AI智能体的质变

OpenClaw这个项目&#xff0c;我从1.0开始就在关注。说实话&#xff0c;最开始它就是个能在我本地跑起来的聊天机器人&#xff0c;接上大模型之后能帮我写写代码、查查资料&#xff0c;新鲜感一过去就吃灰了。但这次2.0发布&#xff0c;社区里到处都在聊“数字员工”&#xff0…

作者头像 李华
网站建设 2026/9/8 19:15:48

基于YOLOv5的火焰烟雾检测:源码数据集与实战部署指南

简介&#xff1a;面向住宅、工业园区、森林、加油站等场景的火焰与烟雾检测需求&#xff0c;这份基于YOLOv5的深度学习资源提供了完整源码与配套数据集&#xff0c;适合具备一定PyTorch基础的目标检测学习者、安全监控开发人员以及相关课程设计团队使用。包内包含约2000个文件&…

作者头像 李华