
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 methodpost label input typetext nameusername / {errors?.username div{errors.username}/div} /label label input typepassword namepassword / {errors?.password div{errors.password}/div} /label button typesubmitLogin/button /Form ); }在测试里如果直接render(LoginForm /)由于既没有actionData、也没有Form所需的上下文测试将无法运行或无法断言错误文案。createRoutesStub会在内存中替你搭一个最小可用的路由环境从而在无服务器、无完整应用的情况下单独测试这类组件。API 形态与返回组件的 PropscreateRoutesStub从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定义见 RoutesTestStubPropsProp作用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 routeloader 数据读取既可以useLoaderData读取也可以从组件 props 接收loaderData如Component({ loaderData })action 与表单提交Form methodpost配合useActionData/ props 中的actionData错误处理Component中抛错会被同路由ErrorBoundary捕获useRouteError/errorprop 均可用fetcheruseFetcher可以加载桩中的其他路由 loader如/api返回自增计数middleware 与 contextmiddleware 可抛redirect完成跳转也可通过RouterContextProvider或直接传普通对象为loader的context注入自定义值meta / linksmeta与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 methodpost.../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 wont 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),仅供参考