
TypeSpec spec-api 实战指南用 typespec/spec-api 构建可验证的 Mock API 场景【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec导读typespec/spec-api是 TypeSpec 仓库中面向 Spector 场景测试体系的轻量级 Mock API 工具包它定义了如何在 TypeSpec 服务规范之外编写「可被自动验证」的模拟端点不仅返回预设响应还能在收到请求时校验其 body、header、query 参数并通过内置的 Matcher 框架对日期时间、URL、大小写敏感字符串等跨语言序列化差异做语义化比较。读完本文你将掌握 spec-api 的完整能力面——从 mock 响应构造、请求校验函数、场景编排 API到match匹配器、dyn动态值模板与 SSE 分块流式响应并能结合 packages/spec-api 的源码理解每条校验背后的实现原理。一、认识 spec-api它在 TypeSpec 生态中的定位1.1 包的基本信息从 packages/spec-api/package.json 可以看到该包名为typespec/spec-api当前版本为0.1.0-alpha.17其描述只有一句话Spec api to implement mock api用于实现 Mock API 的规范 API。它本身是一个 ES Module 包type: module对外只暴露./dist/index.js一个入口运行时依赖只有两个express用于构建 Mock 服务端fast-xml-parser用于 XML 请求体的语义解析与比较。它在仓库中的直接消费者是 packages/spectorSpector 是 TypeSpec 的场景测试框架负责从 TypeSpec 定义生成测试并驱动 mock 服务二者共享同一套 CHANGELOG 条目。例如streamChunks特性同时出现在 spector/CHANGELOG.md 与 spec-api/CHANGELOG.md 中对应同一个 PR #11239。1.2 一句话理解它的价值TypeSpec 描述的是「服务应该长什么样」而 spec-api 描述的是「当请求到达时Mock 服务应该如何响应、以及如何证明这个响应是正确的」。它把「返回数据」和「校验请求」两件事合并到一个 API 定义中让场景测试可以断言请求与响应双方而不只是单向地回吐 JSON。二、核心数据模型MockApiDefinition 与 MockResponse一切场景都建立在一组类型之上它们定义在 packages/spec-api/src/types.ts。2.1 MockApiDefinition一个端点的完整契约export interface MockApiDefinition { uri: string; // 端点路径例如 /payload/pageable/next-page method: HttpMethod; // get | post | put | patch | delete | head | options request?: ServiceRequest; // 可选的请求预期 response: MockResponse; // 预设的响应 handler?: MockRequestHandler; // 可选的请求处理器执行更细粒度的校验 kind: MockApiDefinition; }其中ServiceRequest定义了请求侧的预期export interface ServiceRequest { body?: MockBody | MockMultipartBody; status?: number; query?: Recordstring, unknown; // 期望的 query 参数 pathParams?: Recordstring, unknown; // 期望的路径参数 headers?: Recordstring, unknown; // 期望的 header files?: ServiceRequestFile[]; // 期望的上传文件 }2.2 MockResponse 与 MockBody响应怎么发export interface MockResponse { status: number; headers?: { [key: string]: unknown | null }; body?: MockBody; /** 无论状态码如何都计为成功命中默认仅 2xx 计为成功 */ testSuccessful?: boolean; } export interface MockBody { contentType: string; rawContent: string | Buffer | Resolver | undefined; /** 设置后响应体按多个 chunk 分块流式写出而不是一次性发送 */ streamChunks?: Buffer[]; }MockBody在0.1.0-alpha.17中新增了streamChunks?: Buffer[]字段CHANGELOG 条目 #11590 还提到同期移除了发布包中的构建产物。它的实际消费逻辑在 packages/spector/src/app/request-processor.ts当streamChunks存在时服务端用response.write(chunk)逐个写出每个 chunk最后再response.end()否则走rawContent.serialize(config)后整体response.send(raw)。这一机制正是为 SSEServer-Sent Events分块推送设计的对应 CHANGELOG 0.1.0-alpha.16 中的特性AddstreamChunkssupport toMockBodyfor chunked SSE streaming in mock responses。三、请求校验函数族从 rawBody 到 XML 语义比较spec-api 把「校验请求」实现为一组独立的validateXxx函数全部导出自 packages/spec-api/src/request-validations.ts并统一包装为 packages/spec-api/src/expectation.ts 中RequestExpectation类的实例方法通过MockRequest.expect访问。校验失败时抛出 ValidationError其status固定为 400并携带expected/actual字段toJSON()会输出{ message, expected, actual }。3.1 body 相关校验方法行为rawBodyEquals(raw)用isDeepStrictEqual比较原始 body 字符串/BufferbodyEquals(obj)用matchValues递归比较解析后的 JSON 对象支持内嵌 MatchercoercedBodyEquals(obj)先把日期字符串统一规整为Z结尾再比较如00:00归一到ZbodyEmpty()/bodyNotEmpty()断言 body 为空 / 非空其中coercedBodyEquals的实现值得注意——coerceDate函数通过正则(\d\d\d\d-\d\d-\d\d[Tt]\d\d:\d\d:\d\d)(\.\d{3,7})?([Zz]|[-]00:00)把 UTC 零时区偏移的日期统一替换为Z后再JSON.parse比较从而消除不同语言序列化时00:00与Z的差异。3.2 header 与 query 校验validateHeader(request, headerName, expected)严格比较request.headers[name]与期望值validateQueryParam(request, paramName, expected, collectionFormat?)支持单值与集合两种形态。集合格式由CollectionFormat multi | csv | ssv | tsv | pipes描述内部维护一个splitterMapcsv→,ssv→ 空格tsv→ 制表符pipes→|multi时要求实际值为数组并做isDeepStrictEqual其余格式把期望数组按分隔符join后与decodeURIComponent(actual)比较。3.3 XML 语义校验validateXmlBodyEqualsXML 校验是 spec-api 中实现最完整的部分在 0.1.0-alpha.16 与 0.1.0-alpha.14 两版都有修复。其核心流程request-validations.ts期望值若是普通字符串自动补上?xml version1.0 encodingUTF-8?声明前缀若是xml\...模板生成的 Resolver则直接serialize()使用fast-xml-parser的XMLParser({ parseTagValue: false, ignoreDeclaration: true })解析实际与期望 XML——ignoreDeclaration: true正是 0.1.0-alpha.16 修复的内容XML 声明不再参与语义比较双方声明都会被忽略若期望值内嵌 Matcher如match.dateTime通过getMatchers()收集后把序列化占位替换回 Matcher 对象再走matchValues语义比较否则用isDeepStrictEqual做结构比较。在 0.1.0-alpha.14 中还移除了 prettier 对 ValidationError message 的格式化使错误信息更直接。Spector 的测试 packages/spector/test/xml-validation.test.ts 展示了它的典型用法const body xmlEventwhen${match.dateTime.rfc3339(2022-08-26T18:38:00.000Z)}/when/Event; const body xmlModela${match.dateTime.utcRfc3339(2022-08-26T18:38:00.000Z)}/ab${match.dateTime.rfc7231(Fri, 26 Aug 2022 14:38:00 GMT)}/b/Model;3.4 值格式校验validateValueFormatvalidateValueFormat(value, uuid | rfc7231 | rfc3339)用于断言字符串符合三种格式之一内部各有一组正则见 request-validations.tsuuid^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$大小写不敏感rfc7231HTTP 日期格式如Fri, 26 Aug 2022 14:38:00 GMTrfc3339ISO 8601 时间戳允许小数秒与Z/ 时区偏移。四、Matcher 框架跨语言序列化差异的语义化比较这是 0.1.0-alpha.14 引入的最重要特性PR #10011Add matcher framework for flexible value comparison in scenarios.match.dateTime()enables semantic datetime comparison that handles precision and timezone differences across languages.4.1 原理MatcherSymbol 与 matchValuesMatcher 是特殊的对象可以被放置在期望值树的任意位置。比较引擎 match-engine.ts 通过唯一符号Symbol.for(SpectorMatcher)识别它们isMatcher()类型守卫export interface MockValueMatcherT unknown { readonly [MatcherSymbol]: true; check(actual: unknown, config?: MatcherConfig): MatchResult; // 灵活比较 serialize(config?: MatcherConfig): T; // 序列化占位值 toJSON(): T; toString(): string; }matchValues(actual, expected, path $, config)递归比较时match-engine.ts遇到 Matcher 就调用expected.check(actual, config)而不是严格相等否则按深度相等语义逐层比较数组、Buffer、对象特别地期望对象中值为undefined的键表示「实际响应中不得出现该键」这是「可选字段缺席」的断言方式失败信息会带上path如$.items[0].createdAt便于定位。4.2 内置匹配器一览所有内置匹配器挂在match命名空间下packages/spec-api/src/matchers/index.tsexport const match { string: stringMatcher, // 字符串匹配器 dateTime: dateTimeMatcher, // 日期时间语义匹配器 localUrl: baseUrlMatcher, // 本地 URL 匹配器 };match.dateTimematchers/datetime.ts提供三个变体match.dateTime.rfc3339(2022-08-26T18:38:00.000Z) match.dateTime.utcRfc3339(2022-08-26T18:38:00.000Z) // 只接受 Z 后缀拒绝时区偏移 match.dateTime.rfc7231(Fri, 26 Aug 2022 14:38:00 GMT)其check()分三步先要求实际值是字符串再用格式正则校验形态最后把双方都Date.parse成毫秒时间戳做语义相等比较。因此2022-08-26T18:38:00Z、2022-08-26T18:38:00.0000000Z更高精度甚至2022-08-26T14:38:00.000-04:00等价时区偏移都能与期望匹配而相差一秒的时间会失败——这正是「处理精度与时区差异」的落地方式。测试 packages/spec-api/test/matchers/datetime.test.ts 完整覆盖了这些场景匹配无小数秒、匹配更高精度、匹配00:00偏移、拒绝 RFC 7231 形态、拒绝不同时刻等。match.string.caseInsensitivematchers/string.ts两侧都toLowerCase()后比较。它与 0.1.0-alpha.15 中「encode(string)应用于 boolean」配套——当 boolean 以字符串形式编码时true/false的大小写在不同语言实现中可能不同该匹配器提供共享的大小写不敏感语义。match.localUrlmatchers/local-url.ts创建时只给路径运行时从config.baseUrl注入服务器实际基址后比较baseUrl path用于校验响应中的绝对链接match.localUrl(/payload/pageable/next-page)baseUrl由MockRequest构造时从 express 请求计算得出${request.protocol}://${request.get(host)}见 packages/spec-api/src/mock-request.ts并通过ResolverConfig在序列化时注入。4.3 在 request 校验中正确使用 MatcherSpector 的 CHANGELOG#10259记录了一个易错点修复query 参数匹配必须使用resolveMatchers: false让 Matcher 对象保持原样参与语义比较而不是先序列化成普通字符串再比较。因此在expandDyns展开动态值时Matcher 默认会被解析为toJSON()的普通值用于响应序列化而校验场景下需要保留 Matcher 本体见 response-utils.ts 的ExpandDynsOptions.resolveMatchers。五、动态值模板dyn 与响应构造工具响应侧的工具集中在 packages/spec-api/src/response-utils.ts这是 0.1.0-alpha.5PR #7066新增dyn字符串模板构建器与 0.1.0-alpha.3PR #6565统一请求体与响应体处理迭代的成果。5.1 响应构造json / xml / multipartjson(content, contentType application/json) // JSON 响应体 xml(Roothello/Root) // 纯字符串 XML xmlRootLink${match.localUrl(/next)}/Link/Root // 标签模板内嵌 Matcher multipart({ parts, files, contentType }) // multipart/form-data 响应体xml的标签模板形式会把XML_DECLARATION?xml version1.0 encodingUTF-8?自动前置并把插值中的 Matcher 在序列化时经expandDyns解析。json的createResolver提供两个方法serialize(config)用于发响应Matcher 展开为普通值resolve(config)保留 Matcher 用于matchValues校验。5.2 dyn延迟解析的字符串模板dyn${dynItem(baseUrl)}/payload/pageable/next-pagedynItem(baseUrl)是延迟占位符运行时从ResolverConfig当前仅含baseUrl取值插值可以是 Matcher、其他dyn模板、普通字符串/数字dyn(...)返回的DynValue同时实现了serialize/resolve/getMatchers其中getMatchers递归收集模板中内嵌的所有 Matcher 与其序列化占位——这正是validateXmlBodyEquals做 Matcher 感知比较的数据来源expandDyns(value, config, options)是统一展开入口遇到isDyn的对象调用它、遇到 Matcher 按resolveMatchers决定展开或保留。动态值解析0.1.0-alpha.5 的核心特性使得同一份 mock 定义可以在不同 baseUrl 下复用无需硬编码主机名。六、场景编排passOnSuccess / passOnCode / withKeys场景Scenario定义了「一组 mock 端点被完整调用后测试才算通过」编排 API 在 packages/spec-api/src/scenarios.tspassOnSuccess(apis) // 所有端点被调用且返回 2xx → 通过 passOnCode(code, apis) // 所有端点被调用且返回指定状态码 → 通过 withKeys([a, b]).pass(api) // 指定的 keys 全部被命中 → 通过 withServiceKeys([a, b]).pass(api) // 服务级 keys 变体接受单个或一组端点passOnSuccess对应PassOnSuccessScenariopassCondition: response-successpassOnCode对应PassOnCodeScenariopassCondition: status-code两者是 CHANGELOG 中反复出现的「响应成功/状态码」两类通过条件ScenarioPassCondition response-success | status-code。withKeys/withServiceKeys用于 keyed 场景PassByKeyScenariohandler 返回的KeyedMockResponse携带pass: K | typeof FailFail是Symbol.for(Fail)测试运行器收集所有命中的 key 并与keys列表比对全部命中才判通过。withServiceKeys的差别在于支持传入端点数组适合服务端驱动多端点的场景。此外还有MockRequest类packages/spec-api/src/mock-request.ts把 express 原始请求包装为统一视图headers、query、params、body、files、baseUrl并暴露expectRequestExpectation供 handler 内做声明式校验。七、版本演进时间线CHANGELOG 精读结合 packages/spec-api/CHANGELOG.md 与该包源码可以梳理出这条清晰的能力演进线版本类型关键内容0.1.0-alpha.0 / alpha.1初版仅版本号提升无功能变更0.1.0-alpha.2BreakingNode.js 最低版本提升到 20PR #59770.1.0-alpha.3Features统一请求体与响应体的处理方式PR #65650.1.0-alpha.4Features升级到 express v5PR #69260.1.0-alpha.5Features新增dyn字符串模板构建器支持动态值解析PR #70660.1.0-alpha.6其他Republish无功能变更0.1.0-alpha.7 ~ alpha.13依赖连续多轮依赖升级0.1.0-alpha.14Features FixesMatcher 框架match.dateTimePR #10011移除 validateXmlBodyEquals 中 prettier 的使用PR #99950.1.0-alpha.15Features支持encode(string)作用于 boolean定义大小写不敏感的true/false字符串语义并新增共享的大小写不敏感字符串匹配器PR #108750.1.0-alpha.16Features FixesMockBody.streamChunks支持分块 SSE 流式响应PR #11239XML 声明不再影响语义比较PR #113130.1.0-alpha.17Fixes从发布包中排除构建产物PR #11590其中 0.1.0-alpha.15 的encode(string)boolean 用法在 CHANGELOG 中给出了 TSP 示例model FeatureFlags { encode(string) enabled: boolean; }这意味着 boolean 以字符串形式编码传输配合match.string.caseInsensitive即可对True/FALSE等不同语言的大小写输出做容错断言。八、将 spec-api 用于你自己的场景测试8.1 本地开发与测试在仓库内运行该包的测试vitest 配置见 packages/spec-api/vitest.config.ts# 在 packages/spec-api 目录下 pnpm test # 等价于 vitest run见 package.json 的 scripts测试覆盖了 Matcherdatetime.test.ts、string.test.ts、local-url.test.ts、dyn模板dyn.test.ts、期望引擎expectation.test.ts与匹配引擎match-engine.test.ts是理解各 API 行为最直接的教材。8.2 编写一个带校验的 mock 端点组合示例import { match, dyn, dynItem, json, xml, passOnSuccess, type MockApiDefinition, } from typespec/spec-api; const apis: MockApiDefinition[] [ { uri: /users/{id}, method: get, request: { pathParams: { id: 42 }, headers: { x-api-key: test-key }, query: { verbose: true }, }, response: { status: 200, headers: { content-type: application/json }, body: json({ id: 42, createdAt: match.dateTime.rfc3339(2022-08-26T18:38:00.000Z), profileUrl: match.localUrl(/users/42/profile), }), }, handler: (req) { req.expect.containsHeader(x-api-key, test-key); req.expect.containsQueryParam(verbose, true); req.expect.bodyEquals({ name: Alice }); return { status: 201, body: json({ id: 42 }) }; }, kind: MockApiDefinition, }, ]; // 所有端点均以 2xx 响应即视为通过 const scenario passOnSuccess(apis);要点回顾request字段做声明式匹配路径参数、query、headerhandler内用req.expect.*做命令式校验响应 body 中嵌入 Matcher即可在后续比对中容忍精度/时区/大小写差异match.localUrl需要 baseUrl务必通过dyn/ResolverConfig或 MockRequest 的baseUrl注入不要硬编码主机名。九、边界与注意事项Node 版本0.1.0-alpha.2 起最低 Node 版本为 20当前 package.json 的engines已声明22.0.0本地运行请确保 Node ≥ 22。Matcher 序列化 vs 校验同一 Matcher 在「响应序列化」与「请求校验」两条路径下的行为不同前者展开为普通值后者保留语义注意expandDyns的resolveMatchers开关避免把 Matcher 误序列化成占位字符串。XML 语义比较声明前缀?xml ...?会被ignoreDeclaration: true忽略不要依赖声明差异若期望值中嵌入了 Matcher比较会走 Matcher 感知路径而非纯结构相等。query 集合格式只有显式传入collectionFormat且期望值为数组时才会走集合比较分支multi以外格式会先按分隔符join再与解码后的实际值比较。streamChunks与rawContent互斥从 request-processor.ts 的实现看一旦设置了streamChunks响应将忽略rawContent直接分块写出两者不要同时使用。总结typespec/spec-api用一套紧凑的类型与函数集合把「Mock 响应构造」「请求校验」「跨语言语义比较」「场景通过条件」四件事收敛为可组合的 API。从match.dateTime的语义比较、dyn的动态值解析到streamChunks的 SSE 分块响应每一处能力都能在 packages/spec-api/src 下找到对应实现并在 spector 的场景测试中看到真实消费案例。对于任何希望在 TypeSpec 生态中编写自校验 Mock 服务的开发者这份指南覆盖了从入门到进阶的全部核心接口与实现原理。【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考