速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

基于契约的开发:通过明确需求优化软件开发流程

作者:Naresh Jain

  • 2023-03-14
    北京
  • 本文字数:4266 字

    阅读完需:约 14 分钟

基于契约的开发:通过明确需求优化软件开发流程

独立开发和部署单个微服务的能力是成功采用微服务策略最关键的指标。然而,大多数团队在部署微服务之前必须经历大量的集成测试。这是因为集成测试已经成为识别微服务之间兼容性问题的必要条件,因为单元和组件或 API 测试没有覆盖微服务之间的交互。

 


首先,集成测试是一种发现兼容性问题的后期反馈机制。修复这些问题的成本随着发现时间的推移而成倍增加(如上图底部的热图所示)。

 

此外,这可能会导致客户端和服务端团队做大量的返工工作,严重影响特性交付的可预测性,因为团队不得不兼顾常规的特性开发和集成错误修复。

 

集成环境可能非常脆弱。由于两个组件或服务之间的兼容性问题,即使是单个中断的交互也会导致整个环境受到损害,这意味着即使是其他不相关的功能和微服务也无法测试。

 

这给生产交付造成了阻碍,即使是对关键问题的修复,而且会让整个交付过程陷入停顿,我们称之为“集成地狱”。

 

集成测试——了解这头野兽

 

在终结集成测试之前,我们先来了解它到底是什么。这个词经常被用在不恰当的地方。

 

测试应用程序不仅仅是测试每个函数、类或组件的逻辑。应用程序的功能是这些单独的逻辑片段与其对应部分交互产生的结果。如果两个组件之间的服务边界或 API 没有理清楚,就会导致通常所说的集成问题。例如,如果函数 A 只使用一个参数调用函数 B,而函数 B 需要两个必填的参数,那么这两个函数之间就存在集成或兼容性问题。这种快速的反馈有助于我们尽早纠正并立即解决问题。

 

然而,当我们在微服务级别(服务边界位于 HTTP、消息传递或事件级别)识别兼容性问题时,单元和组件或 API 测试都无法立即识别出任何偏离或违反服务边界的行为。微服务必须与所有实际对应的服务一起测试,才能验证是否存在中断的交互。这些被广泛地(在某种程度上错误地)归类为集成测试。

 

集成测试这个词被用来描述很多类型的检查:

 

  1. 两个或多个组件之间的兼容性;

  2. 工作流测试——涉及交互编排的整个功能;

  3. 与其他依赖项(如存储、消息传递基础设施等)的交互;

  4. 还有更多,生产基础设施的端到端测试除外。

 

需要明确说明的是,当我们说终结“集成测试”时,我们说的是消除对“集成测试”的依赖,不要将其作为识别微服务之间兼容性问题的唯一方法。但其他东西,例如工作流测试,可能仍然是必要的。

确定拐点——知道从哪里下手

 

当所有代码都属于一个单体,方法签名可能就可以作为服务边界的 API 规范。我们可以通过编译时检查等机制强制执行方法签名检查,从而为开发人员提供早期反馈。

 

然而,当一个服务的组件被拆分为多个微服务,服务边界变为接口(如 HTTP REST API)时,这种早期的反馈就不会有了。在之前作为方法签名进行文档化的 API 规范现在需要被显式地文档化,描述清楚正确的调用方法。如果 API 文档不是机器可解析的,还可能会导致团队之间的沟通混乱。

 

如果没有良好文档化的服务边界:

 

  1. 只能使用近似模拟的服务端来构建客户端,而手动模拟和存根技术通常会导致存根过期的问题,即存根无法真正表示服务端。

  2. 对于服务端来说,无法模拟客户端。

 

这意味着我们必须采用缓慢的串行化开发风格,即在开始开发另一个组件之前必须等待其中一个组件构建完成。如果需要快速发布特性,这就不是一种高效的方法。

 

转向微服务后,我们失去了两个关键的能力:

 

  1. 清楚地表示两个组件之间服务边界的 API 规范;

  2. 强制执行描述服务边界的 API 规范。

 

我们需要另一种方法来弥补这两方面的缺失。

API 规范

 

如果想要恢复清晰且按照机器可解析的方式来表示 API 签名的能力,采用 API 规范标准(如 OpenAPI 或 AsyncAPI)就变得至关重要。虽然这增加了开发人员创建和维护这些规范的工作量,但利大于弊。

 

尽管如此,API 规范,顾名思义,也只是有助于描述 API 签名。在开发过程中,为了获得早期的反馈,又该如何强制执行它们呢?这一部分仍然是缺失的。

代码/文档生成——无效且不可持续

 

我们可以认为,我们可以通过代码生成技术来生成和维护 API 规范。从表面上看,如果代码是基于规范生成的,就不会偏离规范。

 

然而,这里存在一些难点:

 

  1. 正在进行中的开发——大多数代码生成工具/技术为服务器端和客户端代码生成脚手架,并要求我们在这个脚手架/模板中填写业务逻辑。问题是,当规范发生变化时,我们通常需要重新生成脚手架,从旧版本的代码中提取业务逻辑,并再次粘贴到新的脚手架中,这增加了犯人为错误的可能性。

  2. 数据类型不匹配——代码生成工具/技术必须支持每一种编程语言。在多语言环境中,生成的脚手架在不同编程语言之间的数据类型(或其他东西)可能不一致。如果我们为一种编程语言生成文档(基于服务端代码生成 API 规范),然后利用生成的规范进一步为客户端代码生成脚手架,这将进一步加剧这种情况的恶化。

 

数据类型不匹配——代码生成工具/技术必须支持每一种编程语言。在多语言环境中,生成的脚手架在不同编程语言之间的数据类型(或其他东西)可能不一致。如果我们为一种编程语言生成文档(基于服务端代码生成 API 规范),然后利用生成的规范进一步为客户端代码生成脚手架,这将进一步加剧这种情况的恶化。

 

总的来说,代码生成和文档生成只能满足有限的场景。虽然它们最初可能通过生成代码为团队提供快捷的构建应用程序的方法,但这种技术的持续成本会让团队不堪重负。

 

因此,我们需要另一种方法来执行 API 规范。

契约驱动开发——API 规范作为可执行契约

 

方法签名可以由编译器强制执行,在开发人员偏离方法签名时向他们提供早期反馈。那么 API 也能实现类似的效果吗?

 

契约测试就是实现这种效果的一种尝试。Pact.io的文档中写道:

 

契约测试是一种测试集成点的技术,它会单独检查每个应用程序,确保它们发送或接收的消息符合记录在“契约”中的内容。

 

不过需要注意的是,契约测试本身也包含了几种方式,例如客户端驱动的契约测试(Pact.io)、服务端驱动的契约测试(生产者契约测试方法中的Spring云契约)、双向契约测试(Pactflow.io)等等。在大多数这些测试方法中,API 契约是独立于 API 规范的文档。例如,在 Pact.io 中,JSON 就是 API 契约。Spring 云契约也有用于定义契约的 DSL。与其维护两个不同的工件(可能会导致不同步),不如利用 API 规范本身作为 API 契约,在开发人员偏离 API 规范导致客户端出现问题时为他们提供早期反馈,这样会不会更好?

 

Specmatic就是这样做的。Specmatic 是一个开源的基于契约驱动开发的工具。它将客户端和服务端之间的交互划分为独立可验证的单元。考虑下面两个微服务之间的交互,目前只在更大级别的测试环境中进行验证。

 

ServiceA <-> ServiceB
复制代码

 

CDD 可以将这种交互分解成连续的组成部分:

 

ServiceA <-> Contract as Stub {API spec of ServiceB}             Contract as Test {API spec of ServiceB} <-> ServiceB
复制代码

 

现在我们来仔细研究一下。

 

  1. 左边:ServiceA => Contract as Stub我们为客户端(ServiceA)模拟服务端(ServiceB),这样客户端应用程序开发就可以独立于服务端进行。由于 Contract as Stub(智能 Mock)是基于双方约定的 API 规范,因此能够真正作为服务端(ServiceB)的 Mock,它会在客户端(ServiceA)调用 API 并偏离 API 规范时给出反馈/抛出错误。

  2. 右边:Contract as Test => ServiceB 为服务端(ServiceB)模拟客户端(ServiceA),并验证响应是否符合双方约定的 API 规范。Contract as Test 将在服务端(ServiceB)应用程序开发人员偏离规范时立即向他们提供反馈。

 

既然我们可以在组件级别让客户端(ServiceA)和服务端(ServiceB)应用程序遵守 API 规范,同时又可以独立构建,那么就没有必要将它们部署在一起来测试它们的交互。这样我们就不需要再依赖集成测试来识别兼容性问题。

 

Specmatic就是这样利用 API 规范作为可执行契约

 


契约即代码

 

这里的关键是 API 规范本身,它可以让 API 提供者和使用者分离并独立地驱动各自组件的开发和部署,同时保持所有组件的一致性。

 

为了成功地进行契约驱动开发,我们需要采用 API 优先的方法,即 API 提供者和使用者需要先协作设计和记录 API 规范。这意味着他们需要使用现代的可视化编辑器之一,如 Swagger、Postman、Stoplight 等来编写 API 规范,在开始独立构建各自的部分之前专注于 API 设计,并确保所有利益相关者保持同步。

 

习惯于基于代码生成 API 规范的团队可能会对这种先编写 API 规范的反向流程感到不适应。CDD 需要类似测试驱动开发的心态转变。在进行测试驱动开发时,我们需要通过先手写测试来指导/驱动代码设计。类似地,在 CDD 中,我们需要先手工编写 API 规范,然后使用 Specmatic 等工具将它们转换为可执行的契约测试。

 

我发现,对于基于代码生成 API 规范的方法来说,API 设计处于次要地位,变得更像是事后的想法,或者是偏向于客户端或服务端。此外,由于发布时间的压力,在采用 API 规范优先的方式时,我们能够并行独立开发客户端和服务端组件,而基于代码生成 API 规范这种方式是不可能做到这一点的(客户端必须等待服务端代码完成并生成了规范)。

 

在就公共 API 规范达成了共识之后,让这些 API 规范有一个单一的真实来源就变得非常重要。如果这些规范出现了多个副本,会导致客户端和服务端团队在实现方面出现分歧。

 

CDD 建立在三个基础支柱之上。“契约即存根(Contract as Stub)”和“契约即测试(Contract as Test)”让客户端和服务端团队保持一致,但将一切联系在一起的粘合剂是第三个支柱——“中央契约存储库”。

 

API 规范是机器可解析的代码,所以还有什么地方比版本控制系统更适合存储它们的呢?将它们存储在版本控制系统(如 Git)中,我们就可以通过添加 Pull/Merge 请求过程来为它们的构建过程增加一些严格性。理想情况下,Pull/Merge 请求应该包括以下步骤:

 

  1. 语法检查,确保一致性;

  2. 向后兼容性检查,确定是否有任何重大变更;

  3. 最后的评审和合并。

 

强烈建议将规范存储在同一个中心位置,这适用于大多数情况(甚至是大型企业)。除非绝对有必要,否则不建议跨多个存储库存储规范。

 


等到规范被存储到了中央存储库中,它们就可以被:

 

  1. 客户端和服务端团队使用,分别进行独立的开发;

  2. 发布到 API 网关。

集成测试的终结

 

我们已经消除了对通过集成测试来识别应用程序兼容性问题的需求,那么系统测试和工作流测试呢?

 

CDD 为更大级别的测试环境铺平了道路,因为所有兼容性问题都在开发的更早阶段(在本地和 CI 等环境中)被识别出来,在这些环境中修复问题的成本要低得多。我们可以通过系统测试和工作流测试在稳定的更大级别的环境中验证复杂的编排问题。另外,由于我们已经不需要通过集成测试来识别兼容性问题,在更大级别的环境中测试套件的总体运行时间也缩短了。

 


原文链接:

https://www.infoq.com/articles/contract-driven-development/

2023-03-14 08:006212

评论

发布
暂无评论
发现更多内容

酷家乐 UI 自动化测试平台实践

CPPAlien

测试框架 selenium BDD UI测试 活文档

Android正确的保活方案,不要掉进保活需求死循环陷进

Halifax

android 大前端 kotlin 移动开发 语言 & 开发

常见的安全应用识别技术有哪些?

郑州埃文科技

Tensorflow保存神经网络参数有妙招:Saver和Restore

华为云开发者联盟

神经网络 tensorflow 变量 Saver Restore

开放原子全球开发者峰会「开源治理」论坛预告(更新中)

开放原子开源基金会

简化IT运维工作,就要学会使用自动化运维工具!

行云管家

运维 云服务 IT运维

大数据包围你我,技术人如何走知识分享之路

华为云开发者联盟

大数据 开发者 技术人 华为云 知识分享

数字化转型的终局:赛博朋克?社会主义?

龙归科技

数字化 软件系统 软件经济 赛博朋克

极客时间架构实战营作业三

jjn0703

架构实战营

Java Stream 源码深入解析

Zexho

Java 源码 stream jdk8

明道云在工程项目行业中的应用场景

明道云

统信软件张磊:国产操作系统如何获得大众市场的认可?

Jessie

开源 最佳实践 新基建 企业动态 文化 & 方法

自动化会提高测试覆盖率,那测试覆盖率是什么?

禅道项目管理

测试 自动化测试 测试覆盖率

手撸二叉树之二叉搜索树中俩个节点之和

HelloWorld杰少

9月日更

我爸电脑上有个加密压缩包,我给用 Python 给解开了

梦想橡皮擦

9月日更

安全系列之:跨域资源共享CORS

程序那些事

Java HTTP CORS 程序那些事 跨域资源共享

对话华为云专家,摆脱无意义“内卷”

华为云开发者联盟

面试 华为云 就业 内卷

只需3步,快来用AI预测你爱的球队下一场能赢吗?

华为云开发者联盟

机器学习 AI 华为云 modelarts 球赛

华为云GaussDB:发挥生态优势,培养应用型DBA

华为云开发者联盟

数据库 开源 GaussDB 云数据库 dba

快速提升Golang编程能力:那就一起用Go做项目吧

博文视点Broadview

研发人员如何进行有效沟通

KJ Meng

研发管理 团队协作 技术沟通 沟通艺术 软素质

Java基础知识查漏补缺

IT蜗壳-Tango

9月日更

鲲鹏展翅|SphereEx 获华为鲲鹏技术认证

SphereEx

小游戏如何应对大流量?Shopee Shake 的大促实践

Shopee技术团队

后端 高并发 游戏 电商大促 Shopee

Vue进阶(幺零九):npm install 遇到 -4048 错误的解决办法

No Silver Bullet

Vue 9月日更

【Flutter 专题】48 图解 Android 原生集成 Flutter Module

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 9月日更

Vue进阶(幺幺零):ant-design-vue

No Silver Bullet

Vue 9月日更

vivo营销自动化技术解密|开篇

vivo互联网技术

Java 后端 软件架构设计 电商营销 平台搭建

【LeetCode】下一个更大元素 IJava题解

Albert

算法 LeetCode 9月日更

必示科技加入云计算标准和开源推进委员会,助力AIOps行业标准建设

BizSeer必示科技

AIOPS 智能运维 必示科技

纵观移动云对象存储发展历程,也少不了 Apache APISIX 的能力加持

API7.ai 技术团队

Apache api 网关 APISIX 企业案例 移动云

基于契约的开发:通过明确需求优化软件开发流程_文化 & 方法_InfoQ精选文章