>科技>>正文

技术干货 | 消费者驱动契约(CDC) 之 SpringCloud Contracts

原标题:技术干货 | 消费者驱动契约(CDC) 之 SpringCloud Contracts

前 言

在TalkingData,我们的所有的线上系统都是以数据为中心的分布式的系统。随着业务系统的增加,如何避免烟囱架构,是大家都在面临的在一个分布式系统中,服务依据能力和角色会被拆分成不同的工程,这些工程可能由一个团队或者相互独立的团队开发和维护,并且它们在系统内部相互依赖,在这种情况下,接口的开发和维护可能会带来一些问题,例如服务端调整架构或接口调整而对消费者不透明,导致接口调用失败。

为在微服务结构下解决类似的问题,Ian Robinson这位大佬提出了一个以服务消费者定义契约为驱动的开发模式:“Consumer-Driver Contracts(CDC)”,既:消费者驱动契约。通常我们开发中主要由服务提供方约定接口,虽然提供方架构调整或改变接口之前通常会通知消费者,但可能还存在上述风险,如果上线出现问题就GG了,而CDC则是以消费者提出接口契约,交由服务提供方实现,并以测试用例对契约进行产生约束,所以服务提供方在满足测试用例的情况下可以自行更改接口或架构实现而不影响消费者。

SpringCloud Contracts Verifier则使基于JVM的消费者驱动锲约的开发应用成为可能。如果你目前使用SpringCloud作为微服务基础环境,那么集成SpringCloud Contracts也是比较好的选择。

SpringCloudContracts Verifier

介绍

SpringCloud Contracts Verifier包含Contract Definition Language(CDL),用来定义的契约用于生成下列资源:

  1. JSON stub:被用作在消费端使用WireMock做集成测试,代码还是要消费端自己写,但是测试数据由SpringCloudContracts Verifier生成。

  2. Messaging routes:如果你用了消息服务,也可以使用SpringIntegration、SpringCloud Stream、SpringAMQP、ApacheCamel来集成。

  3. 验收测试(在JUnit或Spock)用于验证服务器端API的实现是否符合契约。完整的测试由Spring Cloud ContractVerifier生成。

为什么有ContractsVerifier

先来考虑一下下面的情形。假设现在有一个微服务系统

(图片源自Spring官网

那么问题来了,如果我们想测试其中一个服务,比如左上角的服务,它和其他服务有交互,一般我们有两种方式:

  • 部署所有的服务来实现端到端测试。

  • 在集成测试中Mock其他服务。

它们各有优缺点,我们先考虑第一种方式:部署所有的服务。

优点:

  • 模拟生产环境

  • 可以真实的测试服务间交互

缺点:

  • 测试一个服务,你不得不部署其他6个,也许还有耦合的数据库、消息队列等。

  • 测试环境被这一个要测试的服务锁定,你只能测试这一个服务,也就是说其他人无法在同一时间使用这个测试环境。

  • 要运行很长时间。

  • 不能及时的给予测试反馈

  • 很难进行debug

再考虑第二种情况:在集成测试Mock其他服务

优点:

  • 测试反馈非常之快。

  • 没有其他基础服务的依赖要求

缺点:

  • 服务的实现方创建stubs,可能实现与这个并没有关系。

  • 你可能通过测试上生产环境,但是上线后就GG了。

为解决这些问题,SpringCloud Contract Verifier 以及Stub Runner诞生了,其主要的目的是更快速的测试反馈和生成验收测试,不需要去启动其他所有服务,如果你以stubs方式测试,那么只需要直接依赖的应用。

(图片源自Spring官网

ContractVerifier 和Stub Runner的主要目的:

  • 确保WireMock/Messagingstubs 正确mock服务端的接口。

  • 延续ATDD方式和微服务架构风格。

  • 在发生契约变化时,提供一种可立即被服务端和消费端发现的方式。

  • 生成服务端使用的的测试用例。

使用SpringCloud Contracts实现CDC

SpringCloudContracts是CDC框架之一,首先我们先明确具体流程:

消费端:

  1. 消费端编写相关功能的单元测试。

  2. 消费端编写需求的实现。

  3. 将服务端工程clone 到本地。

  4. 在服务端工程中定义契约。

  5. 在服务端工程中添加SpringCloud Contract Verifier插件,创建并安装stubs到仓库。

  6. 在消费端运行集成测试,直到功能符合需求。

  7. 将在服务端工程中编写的契约提交到仓库。

服务端:

  1. check新分支并pull契约。

  2. 创建接口实现。

  3. 编写接口实现,直到验收测试通过。

  4. 合并代码到master并发布到仓库。

最终消费端合并代码到master并发布到仓库,开发工作到此结束。

接下来我们按步骤实现CDC开发流程,我们用一个简单的接口来演示。现假设有一个消费者,需要一个接口用来知道某个设备是否在用户定义的人群中,这个能力需要服务提供方来实现。

那么按照上述流程我们先实现消费端流程的第一项:编写本地单元测试。

首先通过IDE或者 http://start.spring.io 快速创建一个springboot工程client-a。

1. 编写针对需求实现的单元测试。

这里UserLabelsServiceinLables方法没有实现,只返回了true。

2. 实现上述UserLabelsService中的inLables方法。假设服务端的端口为8081,现在还没法运行测试,因为服务端还不存在。

3. 将服务端clone到本地,假设服务端已存在。server不存在则和服务端商议创建。

4. 在服务端项目中定义契约。

需要注意一点,把定义的契约放在src/test/resources/contracts/userlabels路径下,userlabels这一层很重要,因为服务端生成的测试基类名是依据这层的路径名。

现在我们定义一个契约shouldReturnUserInLabels.groovy:

这里契约使用GroovyDSL来定义,想了解更多可以看这个文档 TheApache Groovy programming language - Parsing and producing JSON

5. 在服务端项目中添加SpringCloud Contract Verifier插件,创建并安装stubs到仓库。

这个插件基于契约会做两件事:

  1. 生成并运行单元测试

  2. 产生并安装stubs

作为消费端,你当然不想去生成并运行测试用例,你只需要创建stubs,所以可以跳过测试:

终端会打印一些信息,注意看这里:

生成的stubs被安装到了本地仓库中。

6. 在消费端运行集成测试,直到功能符合需求。

首先添加依赖:

在测试类上添加注解@AutoConfigureStubRunner,需要告诉它group-id和artifact-id,它可以自动下载stubs,设置离线方式。

假设我们的功能现在已经开发好了,就是上述的inLabels方法,现在来运行之前的单元测试,终端会打印如下信息:

可以看到StubRunner下载了在服务端安装的stubs解压到了临时目录,并启动了内嵌的tomcat监听8081端口,注册了相应的servlet,最后所有的stubs都运行起来了。这里看到了一个url映射/__admin/* ,我们访问一下看看返回什么:

可以看到返回了通过我们定义的契约生成的stubjson,其实这是SpringCloudContracts集成了WireMock,它通过stubsjson来mock定义的接口,你也可以通过Wiremock创建一个MockServer,关于WireMock可以看WireMock官网

7. 将在服务端项目中编写的契约提交到仓库。

到目前为止,我们已经完成了在消费端的大部分工作,并且已经确定接口的契约,接下来就在服务端提交我们创建的契约。提交完成后,剩余的工作就交到了服务端。

接下来我们完成server端的工作。我们的服务端工程假设已经存在,不存在则在消费端需要clone时就应该已经创建,接下来我们继续按照上述顺序执行流程。

① check新分支并pull契约

② 创建初始的接口实现

③ 编写接口实现,直到验收测试通过。

添加spring-cloud-starter-contract-verifier依赖,在之前消费端开发流程中已然在服务端添加过了。

之前提到过一个注意的地方:存放契约的路径是在src/test/resources/contracts/userlabels,为什么呢?因为spring-cloud-contract-maven-plugin这个插件会生成对契约的测试类,而测试类会继承一个父类,这个类是由服务端编写,问题是它怎么知道我们写的类名是什么呢?因此约定了命名方式:使用src/test/resources/contracts/下的最后两层路径名称结合并添加Base后缀作为测试父类名,而我们的路径没有两层,所以只会取/userlabels来生成,生成规则为路径名首字母大写+Base,所以我们的测试基类名为UserlabelsBase。

现在我们来创建这个父类:

这里我们使用RestAssured MVC启动服务端UserLabelsApi接口。你也可以设置SpringContext来启动整个服务。

接下来我们执行:

执行完成后,首先在target/目录下看到生成的测试代码,target/generate-test-sources:

以及生成的StubsJson,在路径target/stubs下:

最后我们得到执行结果输出:

可以看到由ContractVerifier生成的测试用例执行成功。对开发者来说,直到功能完成并通过测试,则表示接口开发完成。

④ 合并代码到master并发布到仓库。

此时,我们已经可以合并代码到master,配置要发布的仓库地址并deploy到仓库。

执行完成后,可以看到成功发布到仓库,这里我在本地安装了maven仓库做测试:

接下来我们完成在消费端的最终步骤,合并代码到master,然后需要禁用StubRunner离线方式,指定stubs的地址,它会被StubRunner从仓库自动下载。

如果此时你再次运行测试,会产生一个异常:

莫慌,写的很清楚:本地找到了这个stubs,但是你指定要从远程仓库下载,所以无法判断要用哪一个版本,可以删除本地的stubs,就是.m2/下的,就可以正常运行测试,终端打印如下:

可以看到此时是从远程仓库下载stubs,也就是本地安装的maven仓库。通常我们到此就可以把代码提交并走正常的发布流程,测试用例会在CI等服务器执行,不会出现上述异常。

到此,我们的功能已经开发完成并通过了测试,并且可以发布新版本。

结 语

通过以上流程可以看到:服务端是基于消费端的契约来开发接口,而测试用例由ContractVerifier依据锲约生成,因此就形成了对契约的约束,也就是消费端对服务提供方的约束,如果服务端不能满足测试用例则就不能通过测试。在消费端,开发者也是基于功能而产生的符合自己需求的契约,并编写了单元测试,因此就形成了完整的开发测试流程,并且能更早的发现服务端接口变动,确保了服务的可用性。使用SpringCloudContracts可以满足CDC测试,也有其他成熟的CDC框架:Pact、Janus等,可以对比参考选择。返回搜狐,查看更多

责任编辑:

声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。
阅读 ()
投诉
免费获取
今日推荐