基于PACT框架的契约测试在微服务架构中的应用
背景信息
在软件工程领域,我们经常面临变化。“概念的到来,微服务 ”是那些最近发生的事件,它不仅改变了软件的架构,而且球队的组织方式以及它们如何协同工作之一。
以下是由M. Fowler和J. Lewis引入的微服务的定义之一:“...是一种将单个应用程序作为一套小型服务开发的方法,每种应用程序都在其自己的进程中运行,并与轻量级机制(通常是HTTP资源API)进行通信。这些服务是围绕业务功能构建的,可以通过全自动部署机制独立部署。这些服务的集中管理最少,可以用不同的编程语言编写,并使用不同的数据存储技术。“除此之外,微服务的概念使我们能够灵活地相互独立部署服务。
在这篇博客文章中,我们想概述一下在新的微服务世界中如何测试变化。我们还将介绍消费者驱动合同测试的细节以及支持它的框架。回顾敏捷测试金字塔的众所周知的概念,当我们谈论微服务测试时,我们希望重新检查其中的想法是否仍然适用和有效。
为了更好地理解,我们将使用以下示例模型来描述微服务测试背后的概念:
在上面的图1中,我们可以看到我们有两个微服务,通过REST彼此进行通信。第一项服务扮演消费者的角色,第二项扮演提供者的角色。我们案例中的提供商或产品服务提供了所有与产品相关的信息,例如 名称,类型和说明,按给定的产品标识号码。另一方面,消费者获取数据并消费相关信息。当然可以有尽可能多的消费者。稍后,我们将向两位消费者展示一个例子,但这些应该足以建立理解。
微服务测试方法
单元测试
当我们谈论微服务时,我们是否还应该进行单元测试? - 是的,当然单元测试已经被证明是测试业务逻辑的可靠,快速且不昂贵的方法。但这里只能保证你自己服务的功能。如果您使用消费者微服务,单元测试不能保证提供者服务正常工作。
端到端(系统)测试
当我们谈论微服务时,我们是否还应该进行端到端的测试?- 是的,进行端到端测试很重要,但是当我们谈论微服务时,复杂程度可能非常高。想象一下,为了执行端到端测试,必须部署并运行五个以上的微服务。
集成测试
移动到金字塔的下一层(我们有意留下集成测试) - 我们是否应该在谈论微服务时进行集成测试?- 当然是。有几种方法可以帮助您模拟正在与之通信的微服务,例如服务虚拟化。一些提供这种功能的已知框架是WireMock, Hoverfly。如果您有两个以上的微服务(我们可以假设这是真实世界的项目中的情况),那么在这里进行集成测试的一个缺点是它们的复杂性以及您需要测试和模拟的组合的数量。集成测试不能保证你使用的服务,它应该做的工作。为什么?我们来看一个简单的例子:
您有一个集成测试,其中提供者服务被模拟。在部署消费者服务之前,您希望证明其正常工作。首次运行时,所有测试均为绿色,然后您可以部署您的服务(请参见图2)。
但是,如果您针对生产提供商运行服务,而不是模拟版本,则会失败。在这个例子中,提供者已经改变了数据格式,并且将字段的名称从“ 名称 ”更新为“ 名称 ”。集成测试无法解决这个问题,因为它们正在针对Provider的过时版本运行(参见图3)。
你如何填补你的测试定义中的这个空白?现在介绍消费者驱动合同测试的概念。
消费者驱动的合同测试(CDC测试)
消费者驱动契约方法不过是消费者和提供者之间关于它们在彼此之间转移的数据格式的协议。通常,合同的格式由消费者定义并与相应的提供商共享。之后,测试正在执行,以验证合同是否保存。CDC测试的先决条件之一是可以与提供商服务团队保持良好的最佳密切沟通(例如,当您是消费者和提供商的所有者时)。分享这些合同和交流测试结果是实施适当的CDC测试的重要部分。
协议
PACT是一个开源CDC测试框架。它还提供了广泛的语言支持,如Ruby,Java,Scala,.NET,Javascript,Swift / Objective-C。我们将通过代码示例来介绍两个主要步骤。
Github上的演示项目入门:Pact Demo Github Repo
为了遵循指南,您首先需要实现至少两个微服务。我们的演示项目基于前面介绍的产品案例。另外,我们使用Spring引导来实现微服务实现,使用Gradle作为依赖和配置管理。 在Provider中, 正在使用PACT Gradle插件(有关详细信息,请参阅 https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-provider-gradle)。该指南由两个包含较小步骤的主要步骤组成。第一步着重于消费者应该做什么,第二步是对提供商方面的活动进行分组。
步骤1操作概述
1.1。定义消费者端服务的预期结果
我们从实施ConsumerDemoTest开始,在那里我们指定了Provider的预期结果。有两种主要的方法,扩展基类ConsumerPactTest或使用注释。第二种方法使我们能够灵活地对每个测试类进行多个测试,并消除了继承的必要性,因此可以推荐它作为更好的测试类。在源代码中,您可以看到两个选项。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
public class ConsumerAnnotDemoTest {
@Rule
public PactProviderRule pactProviderRule = new PactProviderRule("product-provider-demo", this);
@Pact(consumer = "product-consumer-demo")
public PactFragment createFragment(PactDslWithProvider pactDslWithProvider) {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json;charset=UTF-8");
return pactDslWithProvider
.given("test demo first state")
.uponReceiving("ConsumerDemoTest interaction")
.path("/product")
.query("id=537")
.method("GET")
.willRespondWith()
.status(200)
.headers(headers)
.body("{" +
"\"name\": \"Consumer Test\"," +
"\"description\" : \"Consumer Test verifies provider\"," +
"\"type\": \"testing product\"" +
"}")
.toFragment();
}
@Test
@PactVerification
public void runTest() throws Exception {
String url = pactProviderRule.getConfig().url();
URI productInfoUri = URI.create(String.format("%s/%s", url, "product?id=537"));
ProductRestFetcher productRestFetcher = new ProductRestFetcher();
Product product = productRestFetcher.fetchProductInfo(productInfoUri);
assertEquals(EXPECTED_NAME, product.getName());
assertEquals(EXPECTED_TYPE, product.getType());
assertEquals(EXPECTED_DESC, product.getDescription());
}
}
|
首先,我们必须为它建立请求和预期的响应。在期望部分,我们说,如果正在执行针对特定URL路径的GET请求,那么预计会出现状态为OK,标头和json正文的响应。之后,在runTest()实现中,我们针对模拟服务执行测试以设置提供程序期望值,并期望产品名称,类型和描述属性的特定值。
1.2。生成PACT文件
现在,我们来运行测试。结果如预期的那样是积极的:
如果我们打开/ target文件夹,我们将能够看到已经创建了一个带有JSON文件的新子文件夹/约定。这实际上是我们与提供商的合同:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
{
"provider": {
"name": "product-provider-demo"
},
"consumer": {
"name": "product-consumer-demo"
},
"interactions": [
{
"description": "ConsumerDemoTest interaction",
"request": {
"method": "GET",
"path": "/product",
"query": "id=537"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json;charset=UTF-8"
},
"body": {
"description": "Consumer Test verifies provider",
"name": "Consumer Test",
"type": "testing product"
}
},
"providerState": "test demo first state"
}
],
"metadata": {
"pact-specification": {
"version": "2.0.0"
},
"pact-jvm": {
"version": "3.3.3"
}
}
}
|
1.3。与提供者服务共享生成的PACT文件
最后,消费者应与提供者分享合同。您可以使用简单文件共享,云服务或PACT Broker。在我们的演示项目中,我们使用了文件共享,但作为一项更先进的技术,值得关注Pact broker。
第2步操作概述
2.1。启动提供程序服务
转到提供者服务。让我们假设我们已经收到了消费者合同,并且我们希望根据真实的提供者实施来验证它。首先,提供者服务应该启动并运行。那里有几种可能性。您可以从您的首选IDE(IntelliJ或Eclipse)启动它,或者使用已经实现的Gradle任务startProviderService。
2.2。针对提供者执行请求
之后,您可以使用JUnit(请参阅ProviderJunitContractTest实现)或Pact Gradle插件:gradle pactVerify来运行验证测试。结果输出:
1
2
3
4
5
6
7
8
|
Verifying a pact between product-consumer-demo and provider1
Given test demo first state
ConsumerDemoTest interaction
returns a response which
has status code 200 (OK)
includes headers
"Content-Type" with value "application/json;charset=UTF-8" (OK)
has a matching body (OK)
|
2.3。检查验证结果
在这种情况下,一切似乎都按预期工作。结果应该与消费者分享,然后给消费者一个绿灯以便在生产中部署其服务。
但是,我们也想监视在发生问题时会发生什么。因此,出于演示目的,我们将向提供商服务注入失败并添加第二个消费者服务。新消费者只使用产品名称和类型,而第一位消费者使用产品名称,类型和说明。这个想法在下面的图6中给出。
我们 通过删除它来更改提供程序属性 说明:
1
2
3
4
5
6
|
public class Product {
private Integer id;
private String name;
private String type;
...
}
|
然后我们运行我们的协议验证步骤(请注意,现在配置的消费者是两个)。结果输出:
1
2
3
4
5
6
7
8
9
10
11
|
Failures:
0) Verifying a pact between product-consumer-demo and provider1 - ConsumerDemoTest interaction Given test demo first state returns a response which has a matching body
$.body -> Expected description='Consumer Test verifies provider' but was missing
Diff:
@1
- "description": "Consumer Test verifies provider",
+ "id": 537,
"name": "Consumer Test",
|
因此,我们可以看到我们违反了合同,但仅限于第一位消费者。这是因为它使用description属性,而第二个不使用它,因此它的合同通过。
概要
微服务的引入为测试带来了新的挑战。实际上,如何测试这种分布式微服务的组合,对用户来说,就像是一项服务。在敏捷测试金字塔众所周知的方法之上,我们可以做的最低限度是开始定义我们的合同。根据消费者API要求,在提供商服务实施之前,这些合同肯定可以编写。消费者驱动的合同测试创建了对支持它的框架的需求。在这篇博文中,我们介绍了PACT,下面我们将讨论 Spring Cloud Contract。