项目:xiaoye-admin-react | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
本文盘点框架的测试资产与质量门禁体系:现有 4 个测试文件测了什么、为什么测这些、门禁命令链如何协作,以及诚实的缺口分析与补测路线。命令速查见《00-项目总览》5.2 与《06-部署运行》6.1,本文在其基础上下沉到"用例级"与"策略级"。
从现有用例的选题可以读出项目明确的测试取向:不追求页面级快照覆盖,而是为"纯函数逻辑"与"关键防御点"立测试。四个测试文件分别对应四类风险——错误分流的分支逻辑(showBizError 按 showType 五路分发,人工回归需要构造五种响应体)、Open Redirect 防护(安全语义,一旦回归就是漏洞)、外部状态订阅(断网横幅的 online/offline 事件)、以及一个真实历史 bug 的回归锚点(Menu 手风琴层级展开)。这个取向的合理性在于投入产出比:中后台框架里最贵的缺陷从来不是"按钮颜色变了",而是"权限兜底放行""redirect 跳到外站""401 不清权限"这类防御逻辑失效,它们恰好都是纯逻辑、可低成本精确断言的。
配套的取向是测试环境与构建环境解耦:vitest.config.ts 独立于 vite.config.ts,源码注释明说"单测只需要 alias 与 DOM 环境,不加载 react / tailwind 等构建插件"。这意味着单测只验证逻辑与 DOM 行为,样式回归(Tailwind 类名、antd 主题)不在测试职责内,也不应混进来——混入的代价是测试启动必须加载整套构建链,速度从秒级退化到十秒级,而收益近乎为零。
这条哲学还有一个隐含推论:能测纯函数的绝不测组件。同样的权限判定逻辑,写在 AuthButton 组件里就需要渲染环境、路由上下文与 store 注入才能测;抽出纯判定函数后一行断言即可。框架当前把权限判定留在组件内(逻辑足够薄,includes 一行),此时测组件是合理的;但二次开发若要扩展更复杂的权限表达式(如支持"或"语义、通配符),正确顺序是先抽纯函数再接组件,让新增逻辑落在可测的一层。src/utils/menu.ts 与 src/api/helper/ 这类纯逻辑目录之所以被既有用例优先覆盖,正是因为它们是"改动频率 × 回归代价"乘积最高且测试成本最低的地带。
vitest.config.ts 全文只有两块:resolve.alias 把 @ 指向 src(与 vite.config.ts 保持一致),test 配置指定 environment: "happy-dom"、include 模式 src/**/*.{test,spec}.{ts,tsx} 与 tests/**、setup 文件 tests/setupTests.ts。选 happy-dom 而非 jsdom 是启动速度与内存占用的权衡;代价是 happy-dom 对部分浏览器 API 的实现不完整,由 setup 文件补齐。
tests/setupTests.ts 承担两件事:引入 @testing-library/jest-dom/vitest 扩展断言集(toBeInTheDocument、toBeEmptyDOMElement 等),以及为 happy-dom 补齐 antd / echarts 挂载时会探测的四个浏览器 API——matchMedia(antd 响应式断点与暗色探测)、ResizeObserver(echarts 容器监听与 antd 布局)、scrollTo(窗口滚动)、URL.createObjectURL/revokeObjectURL(文件上传预览)。这些 polyfill 全部带 if (!window.xxx) 守卫,happy-dom 未来补齐实现后可无损移除。这个 setup 文件是"新增组件测试前先看缺什么 API"的第一站:组件挂载报错时优先怀疑是 happy-dom 缺 API 而非组件自身缺陷。
vitest 5 + @vitest/coverage-v8(覆盖率)+ @testing-library/react(渲染)+ @testing-library/user-event(交互,当前用例未用到但依赖已备)+ happy-dom。测试文件采用就近放置约定:与被测模块同目录的 __tests__/ 子目录(三个单测如此),全仓共享的 setup 放 tests/。命令三个:pnpm test(run 模式一次执行)、pnpm test:watch、pnpm test:coverage。
| 文件 | 被测对象 | 用例数 | 断言焦点 |
|---|---|---|---|
src/api/helper/__tests__/errorHandler.test.ts | showBizError | 6 | showType 五路分发 + 默认分支 |
src/utils/__tests__/safeRedirect.test.ts | getSafeRedirectUrl / buildLoginUrlWithRedirect | 8 | Open Redirect 全变体拒绝 + redirect 编码与防死循环 |
src/components/OfflineBanner/__tests__/OfflineBanner.test.tsx | 断网横幅 | 2 | 在线不渲染 / 离线渲染且恢复即消失 |
src/layouts/components/Menu/__tests__/Menu.test.tsx | 布局菜单手风琴 | 3 | 三层嵌套展开链保持 + 互斥收起 + 重复点击收起 |
errorHandler.test.ts 展示了"为可测性而设计"的范本。showBizError 的返回值就是实际采用的展示方式(源码注释明示此设计意图),测试因此可以不用 spy DOM 而直接断言返回值;同时用 vi.mock 替换 @/hooks/useMessage(模块级单例的 message/notification)与 @/utils/navigate,把"分流"与"跳转"两个副作用都钉死。六个用例覆盖:未携带 showType 时按 ERROR_MESSAGE(兼容历史行为)、SILENT 全静默、WARN_MESSAGE 走 warning、ERROR_MESSAGE 走 error、NOTIFICATION 走 notification.error 且附错误码、REDIRECT 跳登录页且不弹错。这条测试线实质上是在守护"前后端错误契约"——后端改 showType 语义时它是第一道警报。
safeRedirect.test.ts 是安全语义测试的标准写法:正向两条(站内路径、带查询参数的站内路径放行),负向覆盖了攻击面全集——null/undefined/空串、https://evil.com 外链、javascript: 伪协议、//evil.com 协议相对、/\evil.com 与 ////evil.com 解析器变体。buildLoginUrlWithRedirect 侧四条:编码当前地址、保留查询参数、已在登录页原样返回(防死循环)、空路径兜底 / 前缀。任何改动 safeRedirect.ts 的 PR 都应以这套用例全绿为底线,且新增绕过变体时应先加失败用例再修实现。
OfflineBanner.test.tsx 演示外部事件订阅的测法:直接改写 navigator.onLine 属性并派发 online/offline 事件驱动被测组件,beforeAll 中 i18n.changeLanguage("zh") 固定语言避免浏览器语言探测影响文案断言——这是所有含 i18n 文案断言的组件测试都应照抄的前置动作。
Menu.test.tsx 是唯一带"历史 bug 注记"的用例,价值超出其功能范围。它复刻了 authMenuList.json 中全站唯一的二级嵌套结构(商品中心 → 库存管理 → 库存总览),注释写明"手风琴逻辑对它有历史 bug(父级被收起)",第一个用例的关键断言是:展开二级"库存管理"后父级"商品中心"必须保持展开——若父级被收起,子菜单在侧边栏不可见,表现为"点了没反应"。测试设施上它也踩平了两个坑:LayoutMenu 内部使用 useMatches,必须用 createMemoryRouter 包成 data router 渲染;store 状态通过 useGlobalStore.setState / useAuthStore.setState 直接注入而非走真实权限初始化。为布局菜单类组件写测试时直接复用这套脚手架。
项目未配置 husky / lint-staged(README.md 将其列为可选增强),当前门禁是四条命令的纪律性执行,建议 CI 串联顺序:pnpm install --frozen-lockfile(锁文件校验)→ pnpm typecheck(tsc -b,project references 增量检查)→ pnpm lint → pnpm format:check → pnpm test → pnpm build(tsc -b + vite 打包双重验证)。每条命令拦截的缺陷类型不同:typecheck 拦类型破坏与未使用变量(tsconfig 开 strict + noUnusedLocals + noUnusedParameters + verbatimModuleSyntax + noFallthroughCasesInSwitch);lint 拦 Hooks 规则违反、未处理副作用、console 滥用;format:check 拦风格漂移(120 列、双引号、无尾逗号);test 拦防御逻辑回归;build 拦动态导入与模块解析类问题(只有真实打包才会暴露)。
一个值得记住的教训来自 docs/bug-audit.md P0-1 的排查记录:tsc -b 依赖增量缓存(.tsbuildinfo),曾经出现"退出码 0 但 --force 后暴露 4 个错误"的情况——新增文件未被增量检查覆盖时,门禁会给出虚假的绿灯。CI 中建议统一使用 tsc -b --force(或先清理缓存),本地排查"类型明明有错却检查通过"时也应优先想到这一点。
eslint.config.js(逐段解读见《14-配置与环境详解》8.1)中有几条规则直接服务于质量目标:react-hooks/rules-of-hooks: "error" 拦截条件化 Hook 调用(React 19 下后果严重的错误类别);react-hooks/exhaustive-deps: "warn" 提示 effect 依赖缺失(现存 9 条 warning 中 5 条在 useTheme.ts,Menu/index.tsx 的一条缺依赖曾被审计标注为真实逻辑风险);no-console 仅放行 warn/error,保证生产日志可审计;eslint-config-prettier 置于数组最后关闭一切格式类规则,避免 lint 与 format 互相打架。src/services/** 生成物整体豁免,质量门禁只约束手写代码。
诚实盘点,当前测试体系的覆盖是"点"而非"面",以下缺口按风险排序。
第一缺口:请求拦截器管线无测试。 src/api/index.ts 的响应拦截器承载了全项目最关键的分支语义(401 双路径清理、blob 直通、showType 分流、skipErrorHandler 语义),目前只有它调用的 showBizError 有测试,拦截器本体的分支组合(尤其"skip 时仍清登录态但跳过弹提示")没有回归防线。这是全项目最值得补的一个测试文件:用 axios-mock-adapter 或替换 service 实例的 adapter 即可构造任意响应形态。
第二缺口:纯函数层覆盖不均。 src/utils/menu.ts 的三个菜单纯函数(getFlatMenuList/getShowMenuList/findMenuByPath)是路由注册与菜单渲染的事实源,零测试;src/stores/modules/user.ts 与 global.ts 的 persist migrate(版本迁移、异常回退默认值)也零测试——migrate 逻辑一旦抛错会导致登录态整体丢失(源码注释原话),是典型的"低频高危"场景。
第三缺口:路由转换无测试。 ConvertRouter 的 element 字符串 → glob 键匹配、isFull 分流、redirect 转 Navigate,逻辑纯度高、适合测试,且是"菜单即路由"的执行者;它的失败模式(glob 键不匹配时全部渲染 Page Not Found)恰恰是二次开发最容易踩的坑(见《12-FAQ与故障排查》)。
第四缺口:无 E2E 与无 CI 配置。 登录 → 权限 → 建路由 → 渲染菜单这条主干链路只有人工验证;项目也没有任何 CI 配置文件,门禁完全依赖执行者自觉。
第五缺口:MSW handler 与接口契约的一致性无自动校验。 "api 模块 ↔ handler ↔ openapi 三处对齐"目前靠纪律维持,路径改名漏改 mock 时只能在运行期靠 onUnhandledRequest: "warn" 的控制台告警发现。
按"风险 × 成本"给出优先级路线。P1(先补,成本低收益高):utils/menu.ts 三个纯函数(直接传 JSON 数据断言输出,半小时工作量);ConvertRouter 的匹配与分流(可从 Menu.test 复制 data router 脚手架);拦截器 401 分支(含 skipErrorHandler 语义)。P2(随功能补):两个 persist store 的 migrate(构造 v0/v1/v2 旧结构断言迁移结果与异常兜底);fetchAuthButtonsWithRetry 的重试窗口(vitest fake timers 推进 600ms 间隔与 5s deadline);AuthButton 组件(权限码存在/缺失/数组语义三态)。P3(结构性投入):接入 CI(按 4.1 序列,GitHub Actions 或自建 runner 皆可,--frozen-lockfile 是硬要求);Playwright E2E 覆盖"登录 → 出菜单 → 深链刷新 → 登出"主干四步,E2E 里直接复用 MSW(浏览器端 mock 让 E2E 无需真实后端,这是本项目 MSW 架构的额外红利);vitest.config.ts 的 coverage.thresholds 只对 src/utils/** 与 src/api/helper/** 设行覆盖阈值,不给视图层设限,与既有取向一致。
新增测试时有三条通则:优先测纯函数与 store 动作,组件测试聚焦状态转换而非像素;含 i18n 断言先固定语言;用到未实现的浏览器 API 先进 tests/setupTests.ts 补 polyfill 并守卫。
另有一个测试靶点值得单列:MSW handler 自身。handler 是"接口契约的浏览器端实现",它的行为正确性(分页切片、筛选命中数、业务保护规则如内置账号不可删)当前只能在页面上人工点验。由于 handler 是纯请求进、信封出的函数,可以直接调用(构造 Request 对象传入 handler 函数,断言 HttpResponse.json 的载荷)或通过 setupServer(MSW 的 Node 端)+ fetch 测试,成本很低。优先补三类断言:信封一致性(ok() 产物 code===200,与拦截器判定联动,见《03-接口设计》4.2)、分页边界(第一页、末页、total 正确性)、业务保护(system 域的角色引用检查与级联校验)。handler 测试还有一个额外收益:它同时锁住了"mock 行为",避免有人在调页面时顺手改 handler 数据来绕过问题,造成 mock 与真实契约漂移。
这个项目的质量保障现状可以概括为:门禁命令齐备而自动化执行缺位,测试用例少而精、取向明确。四个用例守护的是"错了会很疼"的地方(错误契约、redirect 安全、断网感知、菜单层级),而不是"看起来专业"的覆盖率数字。二次开发者的正确姿势是把这条路线延续下去:每加一个纯函数就配一个测试文件(就近 __tests__),每修一个 bug 先写失败用例(Menu 手风琴用例就是现成范本),接 CI 时把四条门禁命令串成流水线——质量体系不需要一次建成,只需要每次改动都往前挪一格。