本文是“扩展服务开发”系列文章的第四篇,将深入探讨如何为微服务开发构建一套基于模式的 API 测试基础设施。文章将围绕静态 API 模式验证、API 数据工厂、API 模拟框架、API 集成测试框架(AIT)以及 API 测试覆盖率等关键内容展开。

引言
在本系列的第一篇、第二篇和第三篇文章中,我们介绍了如何围绕 Thrift 服务 IDL 构建标准化服务平台,以较低的额外成本,推动并约束所有新服务遵循统一的基础设施标准和工程最佳实践。
虽然这套新架构解决了大型单体代码库中的一些根本性问题,但它也带来了新的挑战,影响了工程效率和软件质量的进一步提升。本文将分享我们在 2019 年围绕微服务测试基础设施所做的一些建设。
为什么模式驱动的基础设施对微服务测试至关重要
在微服务架构中,API 是连接众多组件的边界和桥梁。它定义了服务提供方与服务消费方之间清晰的交互契约。围绕微服务测试和 API 测试,我们观察到了以下几类典型挑战。
1. 破坏性 API 变更
在微服务架构中,API 变更非常常见。然而,我们也曾遇到过一些因 API 变更导致的生产事故。以下内容摘自过往事故复盘报告:
“在相应服务代码变更完成部署之前,新增数据字段导致房源可用性服务的某个 API 出现故障。错误数量激增后,问题部署被自动回滚。”
“只有在确认字段不再被使用之后,才应该弃用模式字段。然而,某个仍在使用字段的移除变更,早于消费方依赖更新完成部署。这一不兼容变更导致相关服务崩溃。”
2. 服务消费方缺少可用的 API 模拟数据
API 往往是测试替身最集中的地方。模拟对象、伪对象、桩对象等测试替身,常常分布在单元测试、集成测试等不同测试场景中。如果缺少统一支持,工程师就不得不在不同地方重复创建模拟数据、模拟客户端和模拟服务,这既耗时又低效。
这还会导致不必要的重型测试设置。例如,为了完成某个测试,工程师可能不得不启动依赖服务,甚至启动虚拟数据库。这样的测试准备工作往往适得其反,因为测试本应足够轻量,并能够被频繁运行。
3. 模拟数据的语义正确性缺乏保障
除了前期创建成本高之外,由于服务业务逻辑会持续演进,手动维护的测试数据很容易与真实服务行为脱节。因此,模拟数据的语义正确性很难长期得到保障。这会导致测试随着时间推移逐渐失效,无法真正发现问题。
4. 服务所有者缺少统一的 API 验证机制
API 是服务与其消费方之间的桥梁。对服务所有者来说,最基本的要求是确保 API 按预期实现。
然而,由于缺少框架级验证支持,服务所有者往往需要投入大量精力,为自己的 API 端点单独配置验证逻辑。而这些验证逻辑,又可能与服务消费方使用的验证集合并不一致。
5. 缺少 API 测试质量的实时指标
最后,工程师几乎无法实时了解 API 测试覆盖率和测试数据质量。对于 API 测试而言,我们缺少类似代码覆盖率这样的通用指标,而代码覆盖率通常被用来衡量单元测试质量。
6. 最重要的是:以轻量且可扩展的方式进行测试
我们最不希望看到的,是鼓励工程师用单体应用的方式构建测试。这类“隐藏的单体测试”会让我们回到原点:测试速度慢、结果不稳定、耦合严重,而且哪怕只是很小的改动,也可能导致大量测试失败。
为了以轻量、可扩展的方式解决上述挑战,我们发现服务模式是关键。
需要说明的是,要让测试体系真正成功,还需要许多关键组件,例如组织层面的培训和最佳实践、可扩展的测试运行器、持续集成基础设施(CI)、持续交付基础设施(CD)、测试环境等。对于正在建设类似测试体系的研发团队来说,也可以借助 PingCode 这类智能化研发管理工具,将团队目标、需求流转、开发、测试、发布和 Wiki 知识沉淀串联起来,并打通研发过程中使用的其他工具;如果团队更关注通用项目协作,也可以通过 Worktile 管理任务、项目、文档、IM、日历、甘特图、工时和审批等协作事项。本文只关注服务 API 测试中与模式相关的部分。
服务所有者和服务消费方如何受益
接下来,我们将深入介绍为服务 API 测试构建的模式驱动基础设施。我们分别为服务所有者和服务消费方构建了以下工具。
对于服务所有者:
- 静态 API 模式验证:自动执行向后兼容性检查和模式检查,尽早发现破坏性 API 变更,并在大规模服务开发中强制执行模式最佳实践。
- API 集成测试框架(AIT):无需编写样板代码,即可验证 API 端点行为,并提供实时 API 集成测试覆盖率指标。
对于服务消费方:
- API 模拟框架:在单元测试和轻量级集成测试中,透明地使用接近真实场景的模拟 API,无需为上游服务创建模拟客户端或模拟端点。
- API 集成测试框架(AIT):使用与服务所有者相同的 API 模拟数据,确保测试语义与服务实现保持同步。
下面我们将进一步介绍这些组件的工作方式。
一、静态 API 模式验证:提前发现破坏性变更
静态 API 模式验证是一种自动检测错误 API 模式变更的工具,主要包含两类检查:
- 向后兼容性检查:重点检测可能破坏 API 兼容性的变更。
- 模式检查器:用于强制执行内部 IDL 最佳实践。
这两类检查非常相似,都需要为所有已变更的模式构建并遍历抽象语法树(AST)。我们创建了一个代码检查工具的二进制文件,可以轻松集成到不同构建系统中,例如 Java 和 Ruby 构建系统。
借助分析模式抽象语法树的能力,我们现在可以捕获破坏性 API 变更,以及不符合最佳实践的模式更新。
静态模式验证的优势
1. 尽早发现并解决破坏性 API 变更
在自动化工具出现之前,这两类检查主要依赖人工代码审查完成。这种方式无法规模化,也容易遗漏问题。
现在,借助静态模式验证,我们能够在代码合并前检测并阻止错误的 API 变更,例如字段类型变更、字段 ID 变更等。
2. 大规模强制执行模式最佳实践
我们可以在无需人工干预的情况下,强制执行内部 API 最佳实践。一个简单例子是,创建一个没有任何值的新枚举类型。这类问题就像一颗定时炸弹:如果枚举名称和值被意外互换,就可能发生序列化错误。
这类错误很难在测试中被发现,而且一旦发生,修复成本很高。得益于静态模式验证,现在我们可以在持续集成(CI)阶段尽早、轻松地捕获这类问题。
二、基于模式的 API 数据工厂
API 数据工厂是一种机制,用于定义与语言无关、经过模式验证的请求和响应测试数据,例如 YAML 文件。它的目标是简化 API 测试数据的创建过程,避免这一过程变得繁琐且容易出错。
如果说模式是 API 的轮廓,那么 API 数据工厂就是 API 的具体画像。它用有意义的数据将 API 具象化,并提供两项基础能力:
- 在单元测试和集成测试中模拟依赖服务 API。
- 使用具体的 API 请求和响应数据验证 API 端点的正确性。
基于这些基础能力,我们进一步构建了能够同时帮助 API 消费方和 API 提供方的测试框架。
测试数据文件的实现
每个 API 数据工厂的 fixture 文件都会与一个特定的 API 端点关联。这种关联对于确保测试数据符合模式规范至关重要。
为了让读者对 fixture 数据有更直观的理解,可以将其看作一组结构化、可复用、可校验的 API 请求和响应样例。这些样例既可以用于模拟上游服务,也可以用于验证真实服务端点。
API 数据工厂的能力
1. 语言无关且灵活
API 数据工厂将与语言无关的 API 数据存储为 YAML 格式,任何程序都可以轻松嵌入和使用这些数据。它还支持多种扩展能力,以满足不同测试场景的需求,例如共享测试数据、灵活匹配、数据断言注解等。
2. 自动化模式验证
每次 API 数据工厂更新时,验证程序都可以自动在本地或 CI 环境中运行,以确保数据正确。如果原始请求或响应数据不符合相应模式,验证程序可以精确定位错误,甚至定位到具体行和列。
3. 用于测试的共享 API 数据
对于服务所有者和服务消费方来说,手动构造 API 测试数据都既困难又繁琐,尤其是在 API 数据结构嵌套很深的情况下。创建共享数据工厂,有助于在提供方和消费方之间复用测试数据,减少重复劳动。
4. 自动化数据生成
此外,我们还开发了相关工具,可以基于匿名化后的生产流量轻松生成测试数据,从而减少手动创建数据的过程,提高生产效率。这也有助于确保模拟数据样例更贴近真实使用场景。
5. API 测试数据质量指标
由于数据工厂是基于模式构建的,我们可以衡量 API 测试数据的质量,例如统计有多少 API 字段被测试数据覆盖和验证。这让我们能够了解模拟数据是否充分,以及 API 测试数据的覆盖程度。
三、API 模拟框架:让微服务测试更轻量
API 模拟框架是嵌入核心服务框架中的一个组件,它提供接近真实场景的模拟 API 交互,用来在测试中替代真实服务。
它主要用于两个场景:
- 单元测试:对包含依赖服务 API 调用的业务逻辑进行白盒测试,但不需要启动依赖服务。
- 浅层集成测试:对服务进行黑盒测试,但不需要启动其依赖服务。
API 模拟框架的能力
1. 无处不在的 API 模拟:客户端或服务端
除了嵌入式模拟客户端之外,也可以让实时运行的服务应用以“模拟模式”进行交互,从而以可靠且低成本的方式获取模拟 API 数据。
当 RPC 客户端不可用时,这种交互方式尤其有用。例如,在前端或移动端开发场景中,可以使用模拟数据测试页面渲染,而无需执行一连串繁琐的真实 API 调用。
当需要进行 API 模拟时,无论是单元测试还是集成测试,都无需创建假的客户端或假的服务。测试程序可以像真实服务调用一样,与内部自动生成的 RPC 客户端交互。模拟模式会在底层通过配置开关或请求上下文标头启用。
换句话说,API 模拟框架透明地封装了 RPC 客户端和服务 API 层,因此可以在任意位置启用模拟能力。
2. 浅层集成测试中的依赖隔离
再次强调我们的目标:在微服务架构中,我们希望避免出现隐藏的单体架构。测试某个服务时,我们不希望同时测试该服务之外的组件。我们将这种测试称为浅层集成测试。
API 模拟框架为依赖隔离提供了一种简洁方案。它目前被用于功能集成测试、隔离负载测试等场景,帮助捕获单个服务中可能存在的回归问题。
3. 按需提供多种模拟选项
模拟模式可以通过配置或 HTTP 标头启用。
启用模拟模式后,从用户视角来看,它与真实服务交互没有区别。在底层,模拟框架会找到第一个与提交请求形成超集匹配的 fixture 请求。
在提交请求之前,也可以通过存根方式指定应该返回哪个 fixture 响应。
此外,还可以为每次模拟交互配置模拟延迟峰值,用于更真实地模拟服务响应延迟。
四、API 集成测试框架(AIT):提升 API 测试覆盖率
什么是 AIT
API 集成测试框架(AIT)允许服务在运行环境中使用预定义测试数据验证自身,无需编写样板代码,同时确保模拟数据具备语义正确性。
其基本流程如下:
- 服务所有者定义一些配置,将验证逻辑与 API 数据工厂中定义的预置测试数据关联起来。
- 目标服务在运行时暴露一个 API 验证网关作为系统端点。
- 该验证网关会将请求路由到相应的、自动生成的端点验证请求中。
AIT 的优势
1. 生产者服务无需编写样板代码即可完成验证
根据测试金字塔,服务端点的正确性是其他复杂端到端测试的基础。理想情况下,在这种模式驱动环境中,测试 API 端点所需要的干净信息,应该只是纯粹的 API 测试数据表示,而不应附带额外的测试设置成本。
需要注意的是,在生产者服务验证过程中,依赖项仍然可以选择被模拟。因此,我们可以用非常轻量的方式完成端点验证。
2. 模拟数据经过语义验证
通过使用 API 数据工厂,我们可以复用为单元测试或其他模拟场景定义的同一份测试数据。
与此同时,生产者服务会使用相同的预定义测试数据和嵌入式断言逻辑进行自我验证,这进一步确保了语义和逻辑的正确性。如果在部署流水线中捕获到意外的实时响应,提供方服务就会被阻断,直到问题得到解决。
这一机制可以一举两得:
- 在 fixture 响应中,带有断言注解的字段始终需要有效并满足断言要求,因此这份数据可以持续用于模拟场景。
- 在真实响应中,同样的断言逻辑也必须通过,从而确保生产环境行为与模拟环境保持一致。
3. 提供实时 API 测试覆盖率检测
AIT 无需手动重复编译,即可实时监控 API 测试覆盖率指标。这不仅有助于团队了解测试的实时健康状况,也能为服务提供方团队成员提供清晰洞察和改进动力,从而随着时间推移持续提升 API 测试覆盖率。
通过这种监控方式,我们不仅可以清楚地了解哪些服务运行正常,还可以进一步了解哪些端点在不同环境下得到了有效验证。
4. 可插拔且轻量
由于 AIT 具备轻量和独立的特点,它可以很容易地与不同 CI/CD 阶段中的测试运行器集成。这种抽象降低了 API 验证与外部环境之间的耦合,也让基础设施的其他部分更容易演进。
总结与展望
强类型模式为构建高效的 API 端点测试基础设施提供了关键抓手。通过共享 API 测试数据和 API 验证逻辑,它将服务提供方和服务消费方连接起来,使 API 测试变得更加准确、有效,并减少重复工作。
模式驱动的测试基础设施不仅能帮助团队尽早发现破坏性 API 变更,还能降低模拟数据维护成本,提升测试数据质量,并为 API 测试覆盖率提供实时可见性。对于正在从单体架构走向微服务架构的工程团队而言,这类能力是保障服务质量和研发效率的重要基础。
文章包含AI辅助创作:微服务 API 测试实践:海外某民宿平台如何构建模式驱动测试基础设施,发布者:su,转载请注明出处:https://worktile.com/kb/p/3981461
微信扫一扫
支付宝扫一扫