如果从契约产生的阶段来说,现有资料表明最早要追溯到西周时期的《周恭王三年裘卫典田契》,将契约文字刻写在器皿上,就是为了使契文中规定的内容得到多方承认、信守,“万年永宝用”。所以订立契约的本身,就是为了要信守,就是对诚信关系的一种确立。诚信,是我国所固有的一种优良传统,也是延续了几千年的一种民族美德,在中国儒家的思想体系里,是伦理道德内容中的一部分。
然而,现在不是这么美好,现实中缺少契约精神的比比皆是
但是,在软件测试领域,契约这把利器,又重新的利用起来
对于测试而言,这个金字塔是理解测试级别的最好的隐喻,这个金字塔最早出于
Mike Cohn 在他的《Succeeding with Agile》,我们从底层往上读
/**
* the function to test
*/
const sum = (a, b) => {
return a + b;
};
/**
* the unit test
*/
test("adds 5 + 5 to equal 10", () => {
expect(sum(5, 5)).toBe(10);
});
但是,显而易见的出现了一个问题
虽然金字塔顶部的测试更能代表客户的体验,
很慢;由于它们遍历多个系统并且通常必须串行运行,因此每个测试可能需要几秒钟到几分钟才能完成,特别是在必须执行先决设置(例如数据准备)的情况下。
难以维护;端到端测试要求所有系统在运行之前都处于正确的状态,包括正确的版本和数据。
可能不可靠或不稳定:由于编排测试环境的复杂性,它们经常会失败,导致误报,从而分散团队的注意力。在许多情况下,它们会由于与任何代码更改无关的配置问题而失败。
难以修复:当端到端测试失败时,由于问题的分布式和远程性质,调试问题通常很困难。
规模严重;随着越来越多的团队的代码得到测试,事情变得更加复杂,测试套件的运行速度呈指数级下降,并且发布在自动化管道中被堵塞。
在流程中发现错误为时已晚:由于运行此类测试套件的复杂性,在许多情况下,这些测试仅在代码提交后才在 CI 上运行 - 在许多情况下,由单独的测试团队在几天后运行。这种反馈延迟对于现代敏捷交付团队来说代价极其高昂。
所以,契约测试就是为了解决这个问题
它们运行速度很快,因为它们不需要与多个系统通信。
它们更容易维护:您不需要了解整个生态系统来编写测试。
它们很容易调试和修复,因为问题只出现在您测试的组件中 - 因此您通常会得到失败的行号或特定 API 端点。
它们是可重复的:
它们可扩展:因为每个组件都可以独立测试,所以构建管道不会随时间线性/指数增长
他们在开发人员机器上本地发现错误:合约测试可以而且应该在推送代码之前在开发人员机器上运行。
所以,契约测试时契约测试是一种软件测试方法,重点验证分布式架构中不同组件、服务或系统之间的交互。这种方法在多个服务或组件由不同的团队开发和维护的场景中非常有用,并且确保它们正确通信和协同工作至关重要。简而言之,契约测试是一种确保两个独立的系统(例如两个微服务)兼容并且可以相互通信的方法。
面向微服务的架构与更传统的整体方法相反。您可以构建松散耦合的服务集合,而不是构建单个软件(例如在服务器上运行的应用程序)。微服务架构具有更小的代码库以及更好的灵活性和可扩展性等优势。
但微服务给测试带来了一些挑战。您可以单独测试每个服务(与集成测试一样),也可以通过端到端测试来测试整个堆栈。
不幸的是,单独测试每个服务并不能保证应用程序对用户来说能够正确运行。如果服务 A 依赖于版本 中的服务 B 的模拟1.4.0,但服务 B 正在切换到1.5.0不同的 API 实现,那么您可以在此级别中断生产而不会出现任何问题。
端到端测试需要您构建一个包含所有所需服务的完整环境,并且测试可能需要几秒或几分钟才能完成,具体取决于复杂程度。因为有很多层,所以最终可能会遇到很多问题,并且很难追踪哪些组件发生了故障。
这就是为什么基于契约的测试在微服务架构中如此常见。
基于契约的测试(CBT)并不是一种新的方法,但这个概念在微服务世界中很容易理解。假设您正在运行一个只有两个微服务 A 和 B 的简单系统:
A 正在消费服务 B。A 是消费者,B 是生产者。服务之间的对话是涉及信息交换的简单 HTTP REST 调用。
A 正在请求有关用户的信息:
GET /users/julien
B 正在提供有关用户的信息:
```python
{
slug: "julien",
fullname: "Julien Bras",
twitter: "_julbrs"
}
这段对话就是一份契约。B 期望使用特定路径 ( /users/{slug}) 进行 HTTP 查询,A 期望答案为带有键slug、fullname和 的JSON 对象twitter。
每个测试都是简单且独立的(仅涉及一项服务),您只需测试每个关系的每一方即可。此测试同样适用于复杂的关系(例如具有多个链接服务的服务或正在使用服务的 Web UI)。
在此之前,我们先来理解一下,这三个关系
消费者(Consumer):对于调用,发起请求的一方。对于MQ,为接收消息的一方。
提供者(Provider):对于调用,响应请求的一方。对于MQ,为生成消息的一方。
契约(Contract):消费者和提供者之间的共识,是一系列交互的集合。对于HTTP调用,包括描述消费者向提供者发送什么的预期请求,以及描述消费者希望提供者返回的最小期望响应。对于消息交互,则描述消费者希望得到的最小期望消息
契约测试主要通过模拟服务间的交互来验证一个服务是否满足与其他服务通信的“契约”。
首先,每一个服务都需要为其外部通信定义一个契约。这个契约包含了服务端需满足的请求格式和预期的响应格式。例如,如果一个服务接受特定的HTTP请求并回应JSON格式的数据,那么这个请求的URL、方法(POST, GET等)、可能包含的请求头、可能的请求体中的字段,并且定义了对应的响应码、响应头以及响应体的内容,所有这些都会在契约中进行定义。
当定义好契约后,就可以进行契约测试了。契约测试主要包括以下两个步骤。
提供者端的契约测试:提供者端的契约测试主要是检查服务是否能够按照契约的规定,正确的处理请求并返回预期的响应。在这个过程中,测试框架会模拟各种请求,然后与契约中定义的响应进行对比,看这个服务是否满足契约。如果任何一个测试请求的响应与契约中定义的响应不符, 所有的契约测试就会失败,并进一步指出不一致的地方。
消费者端的契约测试:消费者端的契约测试主要是检查服务是否能够正确的发出契约中定义的请求,并正确处理预期的响应。在这个过程中,测试框架会模拟服务端,根据契约的定义返回预设的响应,看看消费者是否能够正确处理。如果消费者没能按照契约正确处理这些响应,那么测试也会失败。
对于消费者和提供者的测试,通常会采用一些流行的契约测试工具,例如Pact, Spring Cloud Contract等。
使用这种方式,契约测试可以保证服务间的交互都是符合预期的,而不论系统是否已经部署或者处于什么样的状态,它都只关注单个的服务或者连接,而忽略了系统的其它部分。这使得我们可以在系统的初期就验证服务间的交互是否正确,避免了在部署或者系统运行期间才发现问题,提高了开发和部署时的效率和可靠性。
让我们假设有两个服务:订单服务(Provider)和库存服务(Consumer)。库存服务的角色是在收到订单请求时减少相应的物品数量。这两个服务之间的交互会通过HTTP API进行。
在这个场景中,我们定义的“契约”能够是以下形式:当订单服务向库存服务发送一个POST请求,这个请求包含订单详情(例如,产品ID和数量),如:
POST /inventory/update
Content-Type: application/json
{
"productId": "123",
"quantity": 3
}
库存服务则需要返回一个200状态码,并确认减少的数量,如:
200 OK
Content-Type: application/json
{
"productId": "123",
"quantity": 3,
"status": "success"
}
在这个契约定义好之后,我们就可以进行契约测试了。
在生产者(订单服务)端的契约测试,我们会模拟库存服务发送的请求,然后检查订单服务的响应是否满足契约。比如我们会构建一个请求,包含productId为"123",quantity为3,然后检查返回的响应是否是200状态码,返回的JSON是否包含productId为"123",quantity为3以及status为"success"。
在消费者(库存服务)端的契约测试,我们会模拟订单服务,发送一个包含productId为"123",quantity为3的响应,然后看库存服务是否能够正确处理这个响应。例如,库存服务需要在接收到这个响应后,减少ID为"123"的商品的库存数量3。
以下是订单服务(Provider)的契约测试样例:
from pact import Consumer, Provider
from requests.api import post
# 创建一个Pact对象。Consumer是库存服务,Provider是订单服务。
pact = Consumer('InventoryService').has_pact_with(Provider('OrderService'))
# 定义交互
pact.start_service()
pact.given(
'A request from InventoryService for order update'
).upon_receiving(
'A POST request for order update'
).with_request(
method='POST',
path='/inventory/update',
body={
'productId': '123',
'quantity': 3
}
).will_respond_with(
status=200,
body={
'productId': '123',
'quantity': 3,
'status': 'success'
}
)
# 契约测试
with pact:
result = post(pact.uri, json={'productId': '123', 'quantity': 3})
# 检查结果
assert result.json() == {'productId': '123', 'quantity': 3, 'status': 'success'}
pact.stop_service()
在上面的代码中,我们首先定义了Consumer(库存服务)跟Provider(订单服务)之间的契约。然后我们开始了Provider的模拟服务,并定义了一个交互,这个交互定义了库存服务发来的请求如何以及订单服务的响应应该是什么。最后,我们在Pact的上下文管理器中执行契约测试,发送请求并检查响应是否符合预期。如果所有检查都通过,那么我们就可以确认订单服务满足了与库存服务之间的契约。否则,我们就需要修复订单服务以满足契约。
那么,这个例子中,订单服务是如何处理库存服务发来的请求的?
通常在实际场景中的微服务体系中,订单服务会有专门的路由和处理函数来处理库存服务发来的请求。假设我们使用Flask框架并展示一个简单地处理POST请求的例子
from flask import Flask, request, jsonify
app = Flask(__name__)
# 这个字典用来存储商品的库存信息
inventory = {"123": 10}
@app.route("/inventory/update", methods=["POST"])
def update_inventory():
# 获取请求的JSON数据
data = request.get_json()
# 获取商品ID和需要更新的数量
product_id = data["productId"]
quantity = data["quantity"]
# 更新商品的库存信息
inventory[product_id] -= quantity
# 返回响应
return jsonify({
"productId": product_id,
"quantity": quantity,
"status": "success"
})
if __name__ == "__main__":
app.run()
在以上代码中,我定义了一个路由"/inventory/update",这个路由只接受POST请求。当订单服务接收到库存服务的请求时,会执行update_inventory函数。这个函数首先会解析请求的JSON数据获得商品的ID和需要更新的数量,然后更新库存信息。最后,返回一个包含更新后的信息的JSON数据作为响应。这就是一种可能的订单服务处理函数的实现方式。
契约测试和其他测试的对比
如果您正在管理微服务应用程序,CBT 可以成为您的测试武器库的一个很好的补充。如果使用得当,它可以取代现有E2E测试的重要组成部分。
微信公众号搜索【一个正经的测试】,专注于AI与软件测试技术和宝藏干货分享,每天准时更新原创技术文章,每月不定期赠送技术书籍,让我们在测试会所在测试社区这个大家庭一起学习交流。喜欢记得星标?我,每天及时获得最新推送,
后台回复“软件测试基础”、“AI与大模型“,简历与面试”等领取测试资源,回复“微信交流群”、“内推群”一起进群吹水摸鱼。
个人微信llwfancymyself添加请注明来意 😃