如何用 Vitest 与 Testing Library 为 TanStack Router 代码路由写测试【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router如果你的 TanStack Router 应用使用代码路由用createRoute()手动定义路由、用addChildren组合路由树并且想在 Node 环境中对路由组件、导航、守卫和数据加载写可重复执行的测试这篇指南基于项目文档 How to Set Up Testing with Code-Based Routing 给出完整配置路径用 Vitest 作为测试框架、testing-library/react作为断言与交互工具。文中所有示例均基于 React 版本tanstack/react-router。一点前提Code-Based Routing 指南 中官方注明代码路由不推荐给大多数应用多数项目建议改用文件路由。如果你用的是文件路由本文的工具函数不适用应改用单独的 How to Test Router with File-Based Routing。准备条件安装依赖并配置 Vitest安装测试依赖。文档推荐 Vitest对应命令npm install -D vitest testing-library/react testing-library/jest-dom testing-library/user-event jsdom如果项目已用 Jest文档给出的替代安装命令是npm install -D jest testing-library/react testing-library/jest-dom testing-library/user-event jest-environment-jsdom创建vitest.config.ts文档给出的代码路由配置如下import { defineConfig } from vitest/config import react from vitejs/plugin-react export default defineConfig({ plugins: [react()], test: { environment: jsdom, setupFiles: [./src/test/setup.ts], typecheck: { enabled: true }, watch: false, }, })两个注意点environment: jsdom是必须的路由测试依赖浏览器环境缺失时会报window is not defined见后文常见问题。配置引用了vitejs/plugin-react如果项目中还没有这个依赖需要先安装watch: false表示执行一次后退出不进入监听模式。创建src/test/setup.tsimport testing-library/jest-dom/vitest // ts-expect-error global.IS_REACT_ACT_ENVIRONMENT true第一行引入 jest-dom 的匹配器后面代码里的toBeInTheDocument()断言由它提供IS_REACT_ACT_ENVIRONMENT一行按源文档示例保留。模式一文档推荐TanStack Router 团队内部测试模式文档将这一模式标注为 TanStack Router Internal Pattern (Recommended)即 TanStack Router 团队内部测试路由组件所用的方式。它直接使用createBrowserHistory并在每个用例前后重置 history 和 window 状态整段代码可独立放入一个测试文件import { beforeEach, afterEach, describe, expect, test, vi } from vitest import { cleanup, render, screen } from testing-library/react import { RouterProvider, createBrowserHistory, createRootRoute, createRoute, createRouter, } from tanstack/react-router import type { RouterHistory } from tanstack/react-router let history: RouterHistory beforeEach(() { history createBrowserHistory() expect(window.location.pathname).toBe(/) }) afterEach(() { history.destroy() window.history.replaceState(null, root, /) vi.clearAllMocks() vi.resetAllMocks() cleanup() }) describe(Router Component Testing, () { test(should render route component, async () { const rootRoute createRootRoute() const indexRoute createRoute({ getParentRoute: () rootRoute, path: /, component: () h1IndexTitle/h1, }) const routeTree rootRoute.addChildren([indexRoute]) const router createRouter({ routeTree, history }) render(RouterProvider router{router} /) expect(await screen.findByText(IndexTitle)).toBeInTheDocument() }) })这段代码同时演示了代码路由测试的完整组装过程createRootRoute()建根路由createRoute({ getParentRoute, path, component })建子路由rootRoute.addChildren([...])组合成路由树最后交给createRouter并渲染RouterProvider。模式二封装 renderWithRouter 测试工具对于更简单的用例文档建议封装一个基于createMemoryHistory的渲染工具创建src/test/router-utils.tsximport React from react import { render, RenderOptions } from testing-library/react import { createRouter, createRootRoute, RouterProvider, Outlet, createMemoryHistory, } from tanstack/react-router // 测试用的根路由后续工具与示例都依赖它因此导出 export const rootRoute createRootRoute({ component: () Outlet /, }) // Test router factory export function createTestRouter(routes: any[], initialLocation /) { const routeTree rootRoute.addChildren(routes) const router createRouter({ routeTree, history: createMemoryHistory({ initialEntries: [initialLocation], }), }) return router } // Wrapper component for testing interface RouterWrapperProps { children: React.ReactNode router: any } function RouterWrapper({ children, router }: RouterWrapperProps) { return RouterProvider router{router}{children}/RouterProvider } // Custom render function with router interface RenderWithRouterOptions extends OmitRenderOptions, wrapper { router?: any initialLocation?: string routes?: any[] } export function renderWithRouter( ui: React.ReactElement, { router, initialLocation /, routes [], ...renderOptions }: RenderWithRouterOptions {}, ) { if (!router routes.length 0) { router createTestRouter(routes, initialLocation) } if (!router) { throw new Error( Router is required. Provide either a router or routes array., ) } function Wrapper({ children }: { children: React.ReactNode }) { return RouterWrapper router{router}{children}/RouterWrapper } return { ...render(ui, { wrapper: Wrapper, ...renderOptions }), router, } }与模式一的区别history 用createMemoryHistory测试不会触碰真实window的 URLinitialEntries直接指定初始位置如/users/123、/search?qreact适合按 URL 驱动的用例。注意renderWithRouter要求router和routes至少提供一个否则抛出上述 Error。后文的测试示例都基于这个工具。说明源文档示例中组件内部通过Route这个符号引用测试里创建的路线对象Route.useParams()、Route.useLoaderData()等为让代码自洽可运行下文把路线对象统一命名为Route接入你自己项目时指向对应的路线对象即可。测试路由组件、参数与搜索参数基本渲染与路径参数、搜索参数validateSearchuseSearch可以放在一个测试文件里import { describe, it, expect } from vitest import { screen } from testing-library/react import { createRoute } from tanstack/react-router import { renderWithRouter, rootRoute } from ../test/router-utils describe(Code-Based Route Component Testing, () { it(should render route component, () { function TestComponent({ title Test }: { title?: string }) { return div>import { describe, it, expect } from vitest import { screen } from testing-library/react import userEvent from testing-library/user-event import { Link, createRoute } from tanstack/react-router import { renderWithRouter, rootRoute } from ../test/router-utils describe(Code-Based Route Navigation, () { it(should navigate when link is clicked, async () { const user userEvent.setup() function HomePage() { return ( div h1Home/h1 Link to/about>import { describe, it, expect } from vitest import { screen } from testing-library/react import { createRoute, redirect } from tanstack/react-router import { renderWithRouter, rootRoute } from ../test/router-utils describe(Code-Based Route Guards, () { it(should redirect unauthenticated users, () { const mockAuth { isAuthenticated: false } function ProtectedPage() { return h1Protected Content/h1 } function LoginPage() { return h1Login Required/h1 } const protectedRoute createRoute({ getParentRoute: () rootRoute, path: /protected, component: ProtectedPage, beforeLoad: ({ context }) { if (!mockAuth.isAuthenticated) { throw redirect({ to: /login }) } }, }) const loginRoute createRoute({ getParentRoute: () rootRoute, path: /login, component: LoginPage, }) renderWithRouter(div /, { routes: [protectedRoute, loginRoute], initialLocation: /protected, }) // 应被重定向到登录页 expect(screen.getByText(Login Required)).toBeInTheDocument() }) it(should allow authenticated users, () { const mockAuth { isAuthenticated: true } function ProtectedPage() { return h1Protected Content/h1 } const protectedRoute createRoute({ getParentRoute: () rootRoute, path: /protected, component: ProtectedPage, beforeLoad: ({ context }) { if (!mockAuth.isAuthenticated) { throw redirect({ to: /login }) } }, }) renderWithRouter(div /, { routes: [protectedRoute], initialLocation: /protected, }) expect(screen.getByText(Protected Content)).toBeInTheDocument() }) })验证方式从/protected进入时未认证状态下页面最终渲染的是 LoginPage 的Login Required文本而不是受保护内容。测试 loader 数据加载与错误处理loader 测试的核心是用vi.fn()替换外部数据源再用waitFor等待异步渲染完成import { describe, it, expect, vi } from vitest import { screen, waitFor } from testing-library/react import { createRoute } from tanstack/react-router import { renderWithRouter, rootRoute } from ../test/router-utils describe(Code-Based Route Data Loading, () { it(should load and display data from loader, async () { const mockFetchUser vi.fn().mockResolvedValue({ id: 1, name: John Doe, email: johnexample.com, }) function UserProfile() { const user Route.useLoaderData() return ( div>npx vitest由于配置中watch: falseVitest 执行一次后退出。文档没有给出固定的终端输出示例判定标准就是各用例的断言全部通过对照上文即组件用例findByText/getByTestId/getByText命中预期文本导航用例点击或编程式跳转后router.state.location.pathname、router.state.location.search与预期一致守卫用例未认证时渲染出登录页内容loader 用例waitFor内文本出现且 mock 函数按预期参数被调用。另外配置里启用了typecheck测试运行会附带类型检查Route.useParams()这类带类型的 API 在测试文件里写错参数类型会直接暴露。常见问题排查文档的 Common Problems 一节列了三个与代码路由测试直接相关的失败现象测试报window is not defined确认vitest.config.ts中配置了 jsdom 环境export default defineConfig({ test: { environment: jsdom, }, })组件在测试中访问不到路由上下文原因是直接用 Testing Library 的render渲染了组件没有路由上下文。文档给出的判断对照// 正确带路由上下文的自定义 render renderWithRouter(Component /, { routes, initialLocation }) // 错误裸 render组件内 useLoaderData / useParams 等不可用 render(Component /)异步数据加载导致用例失败断言执行时 loader 尚未完成。解法是等待await waitFor(() { expect(screen.getByText(Loaded Data)).toBeInTheDocument() })下一步文档给出的延伸路径如果你的应用实际使用文件路由参考 How to Test Router with File-Based Routing其中基于生成的routeTree.gen的工具与本文不同测试失败需要定位路由问题时参考 How to Debug Router Issues想覆盖登录流程的完整测试可结合 How to Set Up Basic Authentication。同一篇指南中还包含 Playwright E2E 配置与 React Query 集成测试的示例属于独立测试层按标题范围未展开需要时可直接查阅源文档 setup-testing.md。【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
