2天时间,聊今年最热的 Agent、上下文工程、AI 产品创新等话题。2025 年最后一场~ 了解详情
写点什么

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

作者: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:006665

评论

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

开源一夏 | GO语言框架中如何快速集成日志模块

Masters

开源

Maven中的scope

六月暴雪飞梨花

maven 开源 技术 8月月更 scope

基于微前端qiankun的多页签缓存方案实践

vivo互联网技术

Vue 前端 微前端 qiankun

你认同这个观点吗?大多数企业的数字化都只是为了缓解焦虑

雨果

数字化转型 数字化

mysql进阶(二十一)删除表数据与数据库四大特性

No Silver Bullet

MySQL 8月月更 四大特性 表删除

【LeetCode】设计数字容器系统Java题解

Albert

LeetCode 8月月更

一体化在线政务服务平台,小程序容器技术加速建设步伐

Speedoooo

小程序 小程序容器 政务平台

C++关于函数参数的分析与函数重载进阶教程

CtrlX

c++ 编程语言 后端 cpp 8月月更

流动性质押挖矿系统开发如何制作?单双币系统开发成熟技术

开发微hkkf5566

Gitee图床被屏蔽后,我搭建了一个文件系统并封装成轮子开源!

IT学习日记

Java springboot 签约计划第三季 seaweedfs文件系统 Gitee图床崩了

STM32+MPU6050设计便携式Mini桌面时钟(自动调整时间显示方向)

DS小龙哥

8月月更

开源一夏 | 深入理解 Spring Cloud Gateway 的原理

悟空聊架构

开源 Gateway 认证 签约计划第三季

转转反爬攻防战

转转技术团队

爬虫

博云入选Gartner中国DevOps代表厂商

BoCloud博云

云计算 容器 DevOps 云原生 云平台

10份重磅报告 — 展望中国数字经济未来

阿里技术

研究报告

“纯C”实现——三子棋小游戏

一介凡夫

c 后端、 #开源 8月月更

SAP 云平台上一种 Low Code Development(低代码开发)解决方案

汪子熙

低代码 云平台 lowcode SAP 8月月更

1对1视频源码——快速实现短视频功能提升竞争力

开源直播系统源码

软件开发 直播系统源码 一对一视频聊天系统

kafka_2.13-3.2.0.tgz配置安装

Cjpler

签约计划第三季

阿里前端智能化技术探索和未来思考

阿里技术

前端 智能化

松哥手把手教你在 Vue3 中自定义插件

江南一点雨

Java Vue

OSI 七层模型和TCP/IP模型及对应协议(详解)

Five

TCP/IP TCP协议 8月月更

C++引用分析实例与案例刨析及使用场景分析详解

CtrlX

c c++ 后端 程序员进阶 8月月更

阿里云数据存储生态计划发布,助力伙伴数据创新

云桌派

阿里云

外包学生管理系统架构文档

react常见面试题

helloworld1024fd

React

重磅大咖来袭!阿里云生命科学与智能计算峰会精彩内容剧透

阿里云弹性计算

高性能计算 生命科学 AI制药 智能计算

DVWA 通关记录 2 - 命令注入 Command Injection

Todd-Lee

Geoffrey Hinton:深度学习的下一个大事件

OneFlow

人工智能 神经网络 深度学习

如何在技术上来保证LED显示屏质量?

Dylan

LED显示屏 led显示屏厂家

爆款视频怎么做?这里或许有答案!

博文视点Broadview

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