- 开发工具
- 后端
【免费下载链接】shields
Concise, consistent, and legible badges in SVG and raster format
本篇指南面向所有在 Shields 项目中新增徽章服务或修改现有徽章行为的开发者。文章以 doc/service-tests.md 为骨架,结合 core/service-test-runner/ 下的测试框架源码与 services/docsrs/ 的真实案例,系统讲解如何为徽章编写自动化服务测试、如何运行与调试、如何用 Nock 拦截响应以及如何生成覆盖率报告。读完你将从零写出可被 CI 与代码评审认可的服务测试套件。
为什么要为徽章编写服务测试
在 Shields 中,为新增服务或修改行为编写自动化测试承担着三重职责:
- 验证实现正确性:贡献者与评审者可以快速确认代码按预期工作;
- 监控上游 API 变化:当一个徽章因上游 API 变更而停止工作时,维护者能第一时间发现;
- 降低后续贡献成本:未来的贡献者在调试或改进徽章时,可以直接借助测试快速定位问题。
一份合格的服务测试应覆盖以下四类场景:
- 正常(valid)行为;
- 可选参数,如 tags、branches、version 等;
- 自定义的错误处理逻辑;
- 若服务定义了非平凡的校验器(validator),还需包含对畸形响应(malformed responses)的测试。
服务测试的工程基础:ServiceTester 与 IcedFrisby
Shields 的服务测试建立在IcedFrisby(一个 API 测试框架)之上,并封装了一层服务于徽章场景的抽象。核心类与文件如下:
- core/service-test-runner/create-service-tester.js:约定式地根据
.tester.js文件自动创建ServiceTester实例; - core/service-test-runner/service-tester.js:封装一组测试的
ServiceTester类; - core/service-test-runner/icedfrisby-shields.js:对 IcedFrisby + icedfrisby-nock 的二次封装,提供
expectBadge()、expectRedirect()等徽章专用断言; - core/service-test-runner/runner.js:加载所有 tester 并按需筛选,注册到 Mocha;
- core/service-test-runner/cli.js:命令行入口,负责解析参数、启动本地测试服务器并调用 Runner。
createServiceTester()的实现依赖 Node 的caller模块推断调用文件路径:当从docsrs.tester.js调用时,它会将文件名中的.tester.js替换为.service.js,动态导入该服务模块并取出默认导出的服务类,最后调用ServiceTester.forServiceClass()完成构造(见 create-service-tester.js)。从源码可以看到两点约束:
- 它要求
.service.js模块默认导出单一服务类(且必须是 core/base-service/base.js 的子类),否则会抛出does not export a single service错误并提示直接使用new ServiceTester(); ServiceTester.forServiceClass()会取服务类route.base(如docsrs)作为pathPrefix(见 service-tester.js),这正是测试请求无需写完整前缀/docsrs的原因。
toss()方法负责把测试注册到 Mocha:它会把pathPrefix拼接到 base URL 之后作为每个测试的baseUri,并根据测试是否使用了intercept()自动为测试名加上[live]或[mocked]标记(见 service-tester.js)。
实战教程:以 Docs.rs 徽章为例
我们以 services/docsrs/docsrs.service.js 为例,这个服务展示 Rust 包(crate)的文档构建状态。动手前请先按 doc/TUTORIAL.md 中的 Setup 章节搭建好开发环境。
(1) 搭建测试脚手架
徽章代码位于services/docsrs/docsrs.service.js,测试文件应与之配套存放于services/docsrs/docsrs.tester.js。在测试文件中写入如下样板:
import { createServiceTester } from '../tester.js' export const t = await createServiceTester()createServiceTester从 services/tester.js 导出(该文件同时导出了底层的ServiceTester类,供需要手工构造的场景使用)。由于我们的.service.js默认导出单个类,createServiceTester会按约定自动创建一个针对docsrs.service.js的ServiceTester对象,后续所有测试都挂载到导出的t上。
(2) 编写第一个测试用例
先为最典型的场景(不带 version 参数)添加测试:
import Joi from 'joi' t.create('Docs with no version specified') .get('/tokio.json') .expectBadge({ label: 'docs', message: Joi.equal('passing', 'failing'), })这里有几点关键设计:
create()之后链式调用的方法来自 IcedFrisby,get()用于发起对徽章 URL 的请求;- 测试会真实访问外部服务而不 mock:这与单元测试的惯例不同,但正是服务测试的目的所在——当上游 API 发生破坏性变更时能立刻报警。因此至少应保留一个直接调用真实 API 的测试;
- 为什么请求
.json格式:Shields 上所有徽章都支持多种格式,除https://img.shields.io/docsrs/tokio.svg(SVG 图片)外,还可以请求https://img.shields.io/docsrs/tokio.json获得 JSON 格式。测试用 JSON 格式便于对内容做断言; - URL 前缀自动补全:
createServiceTester()会读取服务类route.base(此处为/docsrs)作为请求的 base URL,所以测试中只需写/tokio.json而不是/docsrs/tokio.json。从 docsrs.service.js 可以看到static route = { base: 'docsrs', pattern: ':crate/:version?' }; expectBadge()的断言能力:其label、message、color等字段既可以是字符串字面量,也可以是RegExp或Joischema。底层实现位于 icedfrisby-shields.js:字符串/数字做严格相等断言,正则用Joi.string().regex()校验,Joi schema 则直接交给Joi.attempt(),其他类型会抛出明确的类型错误;同时它只接受label、message、logoWidth、labelColor、color、link这几个白名单字段;- "图片检查"(picture check)思想:依赖真实服务的测试不应断言某个具体构建状态,而应断言徽章数据符合预期模式。这里用
Joi.equal('passing', 'failing')列出所有合法取值,既能在徽章生成抛错或上游 API 有破坏性变更时让测试失败,又不会因示例 crate 的构建状态变化而误报。
对于更复杂的场景,services/test-validators.js 预置了大量可直接复用的 Joi 验证器:isVPlusDottedVersionAtLeastOne(版本号)、isMetric(1k、2.5M这类指标)、isCommitHash、isStarRating、isPercentage、isDefaultTestTotals等,version、downloads、rank 等常见徽章类型大多已有现成验证器。
另外需要说明:编写 IcedFrisby 测试时通常要调用toss()来注册测试,但在 Shields 中不需要——测试框架会自动调用(见 runner.js 与 service-tester.js)。
(3) 运行测试
运行刚写好的测试:
npm run test:services -- --only=docsrs--only=指定要测试的服务,可传逗号分隔的服务名列表;- 额外的
--让 NPM CLI 把后面的参数原样透传给测试运行器。
预期输出大致如下:
Server is starting up: http://localhost:1111/ DocsRs [live] Docs with no version specified √ [ GET /tokio.json ] (441ms) 1 passing (1s)输出里的[live]标记说明这是一个直连外部服务的测试,Server is starting up: http://localhost:1111/表明测试框架自动在 1111 端口启动了本地测试服务器(见 cli.js)。
当测试失败时,可以开启调试日志帮助定位:
npm run test:services:trace -- --only=docsrs这条命令会输出额外的请求/响应跟踪信息。
(4) 覆盖更多路径:可选参数与自定义错误
Docs.rs 徽章支持可选的 version 参数(pattern: ':crate/:version?',默认'latest',见 docsrs.service.js)。针对确定构建成功的历史版本,可以用字符串字面量收窄期望,失败时能得到更明确的错误信息:
t.create('Passing docs for version').get('/tokio/1.37.0.json').expectBadge({ label: 'docs@1.37.0', message: 'passing', color: 'brightgreen', })注意这里显式断言了color:只有当徽章实现了自定义配色逻辑时才需要显式测试颜色。在 docsrs.service.js 的render()方法中,passing对应success色、failing对应critical色,因此颜色断言在这里是有意义的。
运行结果:
Server is starting up: http://localhost:1111/ DocsRs [live] Docs with no version specified √ [ GET /tokio.json ] (408ms) [live] Passing docs for version √ [ GET /tokio/1.37.0.json ] (171ms) 2 passing (2s)测试多了以后,可以用--fgrep只跑其中一条:
npm run test:services -- --only="docsrs" --fgrep="Passing docs for version"由于 tokio 1.32.1 的文档构建是失败的,可以补一个失败场景的测试:
t.create('Failing docs for version').get('/tokio/1.32.1.json').expectBadge({ label: 'docs@1.32.1', message: 'failing', color: 'red', })接下来覆盖错误路径。Docs.rs 集成在带 version 时定义了 400 状态码的自定义错误(见 docsrs.service.js):
httpErrors: version ? { 400: 'malformed version' } : {},先测试 crate 与 version 均不存在(404)的场景:
t.create('Crate not found') .get('/not-a-crate/latest.json') .expectBadge({ label: 'docs', message: 'not found' }) t.create('Version not found') .get('/tokio/0.8.json') .expectBadge({ label: 'docs', message: 'not found' })再测试畸形 version 触发自定义错误映射的场景:
t.create('Malformed version') .get('/tokio/not-a-version.json') .expectBadge({ label: 'docs', message: 'malformed version' })以上所有用例都已经实际落在仓库的 services/docsrs/docsrs.tester.js 中;该文件还额外包含Multiple builds, latest passing(针对bevy_tweening多构建场景)与Getting latest version works(针对/rand/latest.json)两个用例,可作为完整测试套件的参考范本。
Mocking 响应:用 Nock 拦截上游请求
如果找不到“构建失败的稳定示例版本”,另一种思路是 mock 响应。以Failing docs for version为例,可以改写为:
t.create('Failing docs for version') .get('/tokio/1.32.1.json') .intercept(nock => nock('https://docs.rs/crate') .get('/tokio/1.32.1/status.json') .reply(200, { doc_status: false }), ) .expectBadge({ label: 'docs@1.32.1', message: 'failing', color: 'red', })intercept()来自 icedfrisby-nock 插件,它接收一个 setup 函数并返回 Nock 拦截器,从而暴露 Nock 的完整 API。结合 icedfrisby-shields.js 的源码可知:调用intercept()会同时关闭网络(networkOff())并把该测试标记为intercepted,因此运行输出中会显示[mocked]标记;反之使用get()的测试保持networkOn(),显示[live]。
Nock 非常挑剔:HTTP 方法(GET)、协议(https)、host 与路径必须完全匹配,mock 才会生效。这里 mock 的 URL 与 docsrs.service.js 中fetch()请求的https://docs.rs/crate/${crate}/${version}/status.json一一对应。
在 CI 或对稳定性敏感的场合,cli.js 还支持用SKIP_INTERCEPTED=true跳过所有拦截型测试(见 cli.js)。
更精细的测试运行控制
core/service-test-runner/cli.js 顶部注释列出了测试运行器的全部能力,除了前面用到的--only之外,还包括:
从 stdin 读取服务列表(换行分隔):
echo "service1\nservice2\nservice3" | npm run test:services -- --stdin(
--stdin与--only不能同时使用,CLI 会打印错误并忽略);指定被测实例:
SKIP_INTERCEPTED=true TESTED_SERVER_URL=https://test.shields.io npm run test:services --当设置了
TESTED_SERVER_URL时,测试直接打到该地址,不再自动启动 1111 端口的本地服务器;失败重试(用于应对偶发的不稳定测试):
RETRY_COUNT=3 RETRY_BACKOFF=100 npm run test:services --RETRY_BACKOFF单位是毫秒,对应 IcedFrisbyretry(count, backoff)参数。
另外,Runner 的only(services)方法按服务名前缀(不区分大小写)匹配 tester,并会在没有匹配到任何服务时抛出Unknown services: ...错误(见 runner.js)。本地运行时,测试服务器会在每个用例前调用server.reset()清空请求缓存,避免用例间互相污染(见 cli.js)。
代码覆盖率:确认没有遗漏分支
通过覆盖率报告可以确认测试是否覆盖了所有代码路径:
npm run coverage:test:services -- -- --only=docsrs npm run coverage:report:open第一条命令基于服务测试生成覆盖率数据,第二条命令在浏览器中打开覆盖率报告。结合覆盖率报告检查render()、fetch()与错误分支是否都有对应的测试用例,是提高测试质量的常用手段。
Pull Requests 中的测试约定
PR 必须遵循 CONTRIBUTING.md 中记录的约定,才能执行正确的服务测试集合。Shields 的 CI 运行机制如下(见 cli.js):
- 定时构建:运行全部服务测试;
- Pull Request:仅运行 PR 标题中指定的服务测试,例如标题
[Travis] Fix timeout issues会运行 Travis 相关测试,[CRAN CPAN CTAN] Add test coverage会运行 CRAN、CPAN、CTAN 三个服务。
该机制由test:services:pr脚本分两步完成:先用 core/service-test-runner/pull-request-services-cli.js 解析 PR 标题中的服务名列表(通过 services-for-title.js 实现),再读取该列表运行对应测试。之所以拆成两个独立进程,是因为生成服务列表是异步操作,而 Mocha 的describe.only等独占测试只能同步应用,分步执行既规避了这一限制,也更便于在开发机上调试。
小结与进一步阅读
服务测试是 Shields 徽章质量的守门员:它既验证贡献者的实现,也在上游 API 变动时第一时间发出警报,并为后来者留下可直接运行的行为契约。写作时遵循"真实调用 + 必要 mock + 错误路径 + 畸形输入"的组合,配合--only、--fgrep、SKIP_INTERCEPTED、重试参数与覆盖率报告,即可高效、稳健地完成测试开发与维护。
若对测试编写有疑问,可以在仓库中打开 issue;如果目标徽章已有相关 issue,直接在对应 issue 下评论即可。如需深入,可继续阅读:
- core/service-test-runner/service-tester.js:ServiceTester 完整实现,含
pathPrefix拼接与toss()注册逻辑; - core/service-test-runner/icedfrisby-shields.js:
expectBadge()断言字段白名单与类型分发逻辑; - services/test-validators.js:共享 Joi 验证器集合;
- services/docsrs/docsrs.tester.js:本篇教程对应的完整测试文件(含多构建、最新版本等额外用例);
- IcedFrisby、Joi、icedfrisby-nock 与 Nock 的官方 API 文档。
- 开发工具
- 后端
【免费下载链接】shields
Concise, consistent, and legible badges in SVG and raster format
相关推荐
ESP8266 Deauther隐藏的1.5MB OUI数据库:MAC地址厂商识别原理与更新方法
ESP8266 Deauther隐藏的1.5MB OUI数据库:MAC地址厂商识别原理与更新方法 ESP8266 Deauther 是一款基于廉价 ESP826
嵌入式物联网网络安全渗透测试react-jsonschema-form与Jest测试覆盖率报告徽章
react jsonschema form与Jest测试覆盖率报告徽章 你是否在开发表单应用时遇到过测试覆盖不全的问题?是否想过如何直观展示项目的测试质量?本文
前端UI组件flexivit_base.300ep_in21k vs 传统ViT:19.4 GMACs如何实现更优性能?
flexivit_base.300ep_in21k vs 传统ViT:19.4 GMACs如何实现更优性能? 在计算机视觉领域,视觉Transformer(Vi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考