项目:my-ts-axios | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
这个库的质量保障由五道闸组成,各自拦截不同性质的缺陷:测试闸(Jest 36 个 spec,约 3500 行,带四项覆盖率硬门槛)、静态检查闸(ESLint 9 flat config + Prettier)、提交闸(husky 双 hook:lint-staged + commitlint)、产物闸(scripts/smoke-dist.cjs 以消费者身份验收三类构建产物)、集成闸(GitHub Actions:lint → test → build → examples:build,Node 20/22 矩阵)。一个需要在此明确修正的事实:第 06 篇曾以"未包含 CI 配置 ⚠️ 待确认"存疑,经本次回源核实,CI 配置真实存在(.github/workflows/ci.yml),第 7 节将按其实际内容展开。
与《06-部署运行》的分工:第 06 篇从"怎么跑起来"的角度讲命令与环境;本篇从"质量如何被保证"的角度讲测试体系的设计、代表性用例的实现手法、门槛机制的实现原理与现有缺口。阅读本篇前建议先掌握第 05 篇的核心流程,因为大量用例正是围绕那些流程的边界行为设计的。
test/ 的组织与 src/ 镜像:根级 spec 按"用户可见行为"划分,test/core/ 按链路内部构件划分,test/helpers/ 与 test/cancel/ 按纯函数与取消原语划分。下表为全部 36 个 spec 的分布与规模(行数为当前源码实测):
| 分区 | 文件(行数) | 覆盖域 |
|---|---|---|
test/ 根 | instance.spec.ts(156)、defaults.spec.ts(158)、mergeConfig.spec.ts(125)、transform.spec.ts(94)、headers.spec.ts(92)、xhr.spec.ts(66)、auth.spec.ts(42)、xsrf.spec.ts(54)、cancel.spec.ts(98)、progress.spec.ts(37)、static.spec.ts(42)、batch4.spec.ts(78)、advancedConfig.spec.ts(80)、miscAlignment.spec.ts(97) | 实例与方法族、默认配置与转换器、合并策略、三适配器 headers 拍平、xhr 基础、Basic Auth、XSRF、取消、进度、静态 API 导出面、高级配置(withXSRFToken/allowAbsoluteUrls)、官方对齐项(paramsSerializer 对象形式/env.FormData/NO_PROXY IPv6) |
test/core/ | interceptorRunWhen.spec.ts(58)、AxiosHeaders.spec.ts(102)、settle.spec.ts(65)、getAdapter.spec.ts(30)、AxiosForm.spec.ts(40)、fetchAdapter.spec.ts(304)、httpAdapter.spec.ts(202)、httpAdapterExtras.spec.ts(125)、httpAdapterProxy.spec.ts(247)、httpAdapterEnvProxy.spec.ts(161)、httpAdapterProgress.spec.ts(125)、httpAdapterHardening.spec.ts(145) | runWhen/synchronous、首部类、settle 判定、适配器解析回退、postForm 系、fetch 适配器、http 适配器主体/扩展配置/代理/环境变量代理/进度/健壮性 |
test/cancel/ | CancelToken.spec.ts(88)、Cancel.spec.ts(16) | 取消令牌状态机与取消值判定 |
test/helpers/ | url.spec.ts(154)、headers.spec.ts(97)、data.spec.ts(32)、error.spec.ts(25)、util.spec.ts(117)、cookie.spec.ts(13)、formData.spec.ts(93)、axiosError.spec.ts(44) | 各工具纯函数 |
| 基建 | boot.ts、helper.ts | 全局引导与测试助手(见第 3、4 节) |
从分布可以读出测试投入的重心:http 适配器一个文件就对应六个 spec(约 1000 行),因为它是全库行为最复杂的构件(重定向/代理/解压/进度/健壮性五条线);根级 spec 平均 90 行左右,每个聚焦一个用户可见行为域;test/helpers/ 的用例都很短,因为被测对象是无状态纯函数,输入输出即可断言。这个"复杂度与测试量成正比"的分布本身就是一个健康信号。
Jest 的配置内嵌在 package.json 的 jest 字段:ts-jest 转译、testEnvironment: jsdom、testRegex: /test/.*\.(test|spec)\.(ts)$、setupFilesAfterEach 指向 <rootDir>/test/boot.ts。boot.ts 是整套浏览器行为测试能够运行的前提,它解决的问题是:jest 27 起移除了内置 jasmine 运行器(由 jest-circus 取代),而 jasmine-ajax 这套 XHR mock 生态仍依赖 jasmine 全局。引导层做了三件事(文件头注释完整记录了动机):
jasmine-core 实例:JasmineCore.core(JasmineCore) 并调用 getEnv(),把 JasmineCore 挂到 globalThis.getJasmineRequireObj(jasmine-ajax 向 jasmine-core 注册自身的入口),把实例挂到 globalThis.jasmine(jasmine-ajax 加载时访问 jasmine.getEnv())。jasmineRequire.interface——那会用 jasmine 的 expect/test/describe 覆盖 jest-circus 的同名全局,导致全部用例失效。这条"只借地基、不占房子"的边界是整个兼容层最关键的设计决定,注释中用一句"注意:不要把 jasmineRequire.interface 挂到 global 上"强调。http_proxy/https_proxy/all_proxy/no_proxy(含大写),防止开发机代理劫持 http 适配器的测试请求;需要验证环境变量代理行为的用例(httpAdapterEnvProxy.spec.ts)在用例内自行设置、用完清理。配套的 test/helper.ts 只有一个函数 getAjaxRequest():setTimeout(0) 后 resolve jasmine.Ajax.requests.mostRecent()。它抹平了"发起请求(同步返回 Promise)与 XHR open/send 发生"之间的微小时序差,是几乎所有 xhr 系用例的第一行引用。
npm test 实际执行 jest --coverage,覆盖率不是"参考数字"而是失败条件——package.json 的 coverageThreshold.global 设了四项:branches 80 / functions 85 / lines 90 / statements 90,任何一项任何文件均值不达标,jest 以非零码退出。三个配套字段共同决定统计口径:collectCoverageFrom: ["src/*.{js,ts}", "src/**/*.{js,ts}"] 让从未被任何测试 import 过的源文件也进入统计(否则未测文件默认不计入,门槛形同虚设——这是"测试即门禁"能成立的技术前提);coveragePathIgnorePatterns 排除 node_modules、test 与 src/shims(占位抛错代码没有可测分支,排除是合理的口径声明而非作弊)。实践闭环:npm test 后查看 coverage/lcov-report/index.html 定位未覆盖分支,新写的适配器/helper 直接对照报告补测;源码中的 /* istanbul ignore next */ 注释(如 xhr.ts 第 171 行 processCancel 的兜底 catch、fetch.ts 第 200 行 emitProgress 的短路分支、helpers/error.ts 第 27 行构造器)标记了少数"结构上不可达或需要模拟极端环境"的分支,属受控豁免。
四项门槛的梯度也值得解读:lines/statements 定在 90%(最高),functions 85%,branches 80%。这个梯度承认了"分支全覆盖"在同构库里成本最高(三适配器 × 多环境组合的分支爆炸),而"行覆盖 90%"配合 collectCoverageFrom 的强制纳入,已经足以保证"新增文件不写测试会直接跌破门槛"。
六个用例锁定三件事:AbortController abort 后以 code: 'ERR_CANCELED'、message: 'canceled' reject(新轨取消);400 → ERR_BAD_REQUEST 且 error.status === 400;500 → ERR_BAD_RESPONSE 且 status === 500(旧语义里这两个码曾在此类实现中互相反转,settle.spec 的注释同样记录了这次回归)。实现手法是标准的 jasmine-ajax 模式:beforeEach install / afterEach uninstall,发起请求后 getAjaxRequest() 拿到 mock XHR,respondWith({ status, responseText }) 注入响应,再对原 promise 断言。afterEach 里还演示了 XSRF cookie 的清理(把 XSRF-TOKEN cookie 置为过期时间删除),避免用例间状态泄漏。
settle.ts 是纯函数,测试因此完全绕开适配器:makeResponse(status) 工厂造响应,jest.fn() 造 resolve/reject 间谍,五个用例分别覆盖 2xx resolve、4xx/5xx 的码与 status 写入、无 validateStatus 时任意状态放行、自定义 validateStatus: s => s < 500 放行 404。这个文件展示了本仓库单元测试的理想形态——被测对象无依赖时,测试不引入任何环境。
runWhen 两条用例(返回 false 时拦截器不被调用 / true 时被调用)都通过 jest.fn(c => c) 间谍断言;synchronous 用例则利用了同步快路径的时序特征:不 await instance.request(),紧接着同步断言 sideEffect === true——只有同步快路径能让拦截器在 request 返回前执行完毕,异步路径下断言必然失败。用"是否需要 await"来区分同步/异步行为,是这个文件最巧妙的手法。被测对象直接 new Axios({ method: 'get' }) 构造最小实例,不经 axios.create,验证的是类本身。
spyRequest 用 jest.spyOn(instance, 'request').mockResolvedValue(...) 把被测实例的 request 替换为间谍,然后断言传给 request 的 config:method 正确、Content-Type 被前置为 multipart、data 已是 FormData 且 .get('name') 可读、formSerializer: { dots: true } 透传后 data 键名变为 user.name。这种"截断在 request 入口"的手法让 postForm 系测试完全摆脱网络与 jasmine-ajax,只验证参数折算逻辑本身——与 5.3 共同构成"编排层测试不碰 I/O"的惯例。
test/core/ 下所有 httpAdapter spec 都带 /** @jest-environment node */ 文档注释切换到 Node 环境(默认 jsdom 没有 http 模块语义),并在 beforeAll 里 http.createServer(...).listen(0) 起随机端口的真实服务器(AddressInfo.port 取端口),afterAll 关闭。以 httpAdapter.spec.ts 为例,服务器预置了 /json(JSON 回显)、/echo(请求体回显)、/redirect(301 → /json)、/r/<code>(任意状态码重定向端点)、/gzip(gzip 压缩体)、/slow(200ms 延迟,超时用例)等端点,随后用 axios.get(baseURL + '/json', { adapter: 'http', proxy: false }) 走真实 HTTP 栈断言。httpAdapterHardening.spec.ts(145 行)是安全语义的专项锁定:双服务器结构(serverA 重定向到 serverB)验证跨 host 剥离 Authorization/Cookie、Readable 流式请求体超 maxBodyLength 中止且报 ERR_BAD_REQUEST、未超限正常送达、5xx 的码与 status 三断言。httpAdapterProxy.spec.ts 与 httpAdapterEnvProxy.spec.ts 则分别验证 config.proxy 对象(含 HTTPS CONNECT 隧道)与环境变量推导/NO_PROXY 匹配。**"用真实服务器测真实协议栈、用 mock 测传输之上的语义"**是这套测试的分工原则。
文件名直译"杂项对齐",实际是官方行为对齐项的回归登记:paramsSerializer.indexes 三态的精确输出(foo[]/foo[0]/foo)、自定义 encode 的大写编码结果、serialize 优先于 encode/indexes 的次序、defaults.env.FormData 注入的构造器被 multipart 自动序列化优先使用、getProxyForUrl 对 IPv6 NO_PROXY 条目的匹配。每当跟随官方新增一个对齐项,就在这里加一组用例——它扮演着"差异清单的测试镜像"角色。
scripts/smoke-dist.cjs 挂在 prepublishOnly 链尾(npm test && npm run build && node scripts/smoke-dist.cjs),设计动机写在文件头:此前曾因 CJS 输出 named 导出发出过 require() 拿到 { default: ... } 包装的坏包。它按消费矩阵逐项验收五件事:①CJS require(dist/node/axios.cjs) 得到函数本身,且 create/CancelToken/CanceledError/AxiosError/isCancel/isAxiosError/HttpStatusCode/mergeConfig/getAdapter/toFormData/formToJSON/all/spread/Axios/VERSION 15 个导出逐一存在;②UMD 产物以 require 方式加载(打包器场景)形状一致(函数或 .default);③ESM 产物做文本级检查含 export default;④dist/types/index.d.ts 存在;⑤axios.VERSION === package.json.version(注释记录了 VERSION 与发布版本脱节的教训)。脚本末尾打印 smoke-dist: OK (v…)。局限同样明确:ESM 检查是正则而非运行时 import(避免在 CJS 脚本里动态 import 的复杂性),类型声明只验存在不验内容——这两点在第 7 节列为补强建议。
提交闸:package.json 的 prepare: husky 在 npm install 时安装 hook;.husky/pre-commit 只有一行 npx lint-staged,对暂存的 {src,test}/**/*.ts 依次 prettier --write 与 eslint --fix(改动自动 re-stage);commit-msg hook 由 commitlint(@commitlint/config-conventional)校验提交信息前缀;npm run commit 走 commitizen 交互式生成合规信息。
集成闸(.github/workflows/ci.yml,修正第 06 篇存疑):触发条件为 push/PR 到 main;verify job 在 ubuntu-latest 上以 Node 20 与 22 双版本矩阵执行五步——actions/checkout@v4、setup-node@v4(带 npm 缓存)、npm ci(显式 HUSKY: 0 避免 CI 环境安装 git hook)、npm run lint、npm test(步骤名明确标注 "with coverage thresholds",即覆盖率门槛在 CI 同样生效)、npm run build、npm run examples:build。这个编排意味着每次 PR 都要过"静态检查 + 全量测试与覆盖率 + 库构建 + 示例构建"四道验证。
现有缺口与补测建议(按优先级):
npm audit --audit-level=high 步骤(详见《08-安全与权限设计》第 6 节)。import() 在构建后真实加载一次 ESM 产物(Node 20 支持 --input-type=module 或改写脚本为 mjs),把"导出形态正确"从字符串匹配升级为运行时事实。coverage/;可接 Codecov/Coveralls 或在 CI 中输出 job summary,让"哪个文件拖低了覆盖率"在 PR 页可见。prepublishOnly 链路完整但依赖手动 npm publish;可补 GitHub Actions release workflow(tag 触发 → npm publish → 产出 GitHub Release),把 smoke-dist 的通过记录留存。ontimeout、withCredentials 等真实浏览器行为覆盖有限(无真实浏览器测试);可评估 Playwright 跑 examples/ 的关键页面作为端到端补充——examples 体系(八组 mock 路由 + 8088 跨域服务器)已经具备成为 E2E 沙盒的全部条件。--ci 标志与快照治理:当前未用快照测试,无此负担;若未来引入,记得在 CI 传 --ci 防止快照意外写入。这套质量体系的核心设计是"门禁化":覆盖率不是目标而是失败条件(collectCoverageFrom 强制纳入使门槛不可逃避),产物正确性不是构建副产品而是消费者视角的断言(smoke-dist),提交不是自觉而是 hook 拦截(husky 双闸),远端不是"应该有人看"而是每次 PR 自动验证(CI 矩阵)。薄弱面集中在生态侧——依赖审计缺位、ESM 冒烟是文本级、无真实浏览器 E2E、无发布自动化——都属于"加几个配置"量级的补强,不动测试架构本身。给贡献者的最短验证清单是:npm run lint → npx tsc --noEmit → npm test(盯覆盖率报告)→ npm run build → node scripts/smoke-dist.cjs,全绿再提交。