浅谈契约测试
在现代的开发模式中,基于微服务的开发模式越来越常见,但是随着项目规模的扩大,服务与服务之间的依赖越来越密切,当不同的开发团队去开发不同的服务时,服务的提供者的变动会影响到众多消费它的消费者,为了保证系统的正确性和一致性,这将需要大量的沟通成本和代码修改的时间成本。
之前遇到的某个客户内部就是因为服务与服务之间依赖过多,且存在各种的物理依赖,再加上其他种种原因,使得在集成测试时bug激增。对于他们而言集成测试需要依赖于各个服务版本的一致性以及真实的物理环境,因此他们的集成测试通常需要用上几个小时才可以完成,这就使得整体的效率大大缩减。除此之外,在集成测试中发现的问题也会使得他们花很长的时间去定位到问题所在。
相似的问题在平时的开发过程中也是经常遇到,由于依赖方的接口变更导致在系统集成时频频出错,整体的代码又不得不再加修改,这就使得开发的进度迟迟无法向前推进。
为了解决这类的问题,契约测试应运而生。契约测试不是一个新鲜东西,但在实际项目经历中发现用好契约测试真的会大大增强开发的效率,因此写下这篇文章来简单总结一下契约测试的一些内容。
契约测试是一个为确保两个独立的系统或者微服务能够兼容并可以相互通信的一个方法,契约测试分为两种,一种是服务提供者驱动的,另一种是消费者驱动的。如下图所示,左侧是一个服务的消费者,右侧是一个服务提供者,消费者调用提供者的接口并消费数据的交互过程会被记录成一份契约,在契约中包含了服务的提供者和消费者是谁,以及消费者对服务的提供者的期望(如请求的参数和返回的结果)。服务的提供者会根据这份契约去反复验证自己是否能够满足消费者的需求,这也就是所谓的消费者驱动。
契约测试主要是为了验证服务层提供的数据是否能够消费者正常使用,它不会深入去测试服务的行为,而只是专注于测试服务的输入与输出,因此相比于沉重的集成测试而言,契约测试会更加的轻巧,快速。契约测试形式上类似于API级别的UT,但其本质上还是个集成测试,比API测试在金字塔的位置更靠顶端,所以容易导致契约测试的数量增加和不稳定性增加。
接下来我们分别从代码和流水线设计两方面来阐述一下具体的契约测试的实践:
代码层面:
为了完成契约测试,我们可以借助一个叫pact的工具。pact是一个代码优先的用来支持契约测试的一个工具,它目前支持java,python,go等主流的开发语言。
Pact中的一些基本概念:
Contract: 契约文件,在Pact中也叫做pact,可以保存在本地,也可存在broker中
Provider: 真正运行的生产者服务
Consumer: 接收生产者发出的数据
在pact中,consumer和provider分别做了不同的事:
Consumer端:
consumer端会做这么几件事:
首先使用pact dsl定义它消费的接口的request和response,并注册到mock server中
然后consumer端的测试会发送一个真实的请求到pact起的一个本地的mock server
接着pact会去对比实际的request和expected request 是否一致,如果一致则返回expected response
最后consumer会去确认这个返回值是否正确 上面所有步骤都pass后,整个的consumer测的pact测试才算结束,此时consumer定下的契约会被发布到一个叫pact broker的地方进行契约的统一管理。
Pact broker是pact提供的一个专门用来统一管理契约的一个服务,在这个服务中,开发者们可以清晰的看到所有的服务提供者和消费者的详细信息。
总的来说,cousumer端的主要功能是生成契约(文件的载体),验证request和response的工作是可选的,借由consumer端的集成测试的形式,确保生成的契约的确是consumer真正期望的,通俗来讲,就是“测试测试的测试”。
Provider端:
在provider端,pact会mock出一个consumer并发送请求给provider端真实运行着的进程,provider在接受到请求后会根据自己的代码实现将真实的response返回给pact,接着pact会拿着这个response去和pact broker上获取到之前consumer定义的契约并进行比对,如果provider能够满足契约,则验证通过。
当consumer和provider的测试都通过后,产品则就可以被部署到指定环境了。
以上是消费者驱动的一个实践方式,消费者驱动的契约测试主要适用于以下场景:
消费者和提供者都是可控的
消费者的需求变动能够变成提供者的需求
消费者数量不是很多,作为提供方能够管理的过来
符合以上的条件的场景下,比较适合使用消费者驱动的契约测试。消费者驱动的背景下,服务提供方可以基于消费者提出的契约快速做出反馈。
然而,在实际的情况可能不是这么美好,之前遇到的客户,他们内部的部分情况恰恰违背了以上的场景。他们的产品极度依赖着一些外部的底层依赖,且底层的依赖变动频率较高,这使得他们会频频的在集成测试时发现底层已经发生了变动。在这种情景下,提供者驱动的契约测试更加适合。由服务的提供方来约定契约,然后众多的消费者去满足契约,当提供方发生变动时,消费方能够及时感知到并快速反馈。整体的实践流程只需将上方的consumer者和provider的操作进行转置即可。
换句话说,消费者驱动和提供者驱动的区别在于谁去响应契约的变化。就如上方提到的,外部的提供者依赖是不可控的情况下,提供者驱动的模式会更加合适,相反则是消费者驱动的模式。
流水线的设计
当选择消费者驱动的契约测试策略时,作为一个consumer,它要做的就是去发布契约,告诉provider它的需求。那么作为provider,它就需要去检查自己的实现是否能够满足consumer的需求,那么当它的实现无法满足契约时,则此时的流水线契约测试阶段就应该显示fail,并告知对应的provider,让其快速做出修正 。如图所示,当consumer发布了新版本的契约,这将导致provider端的流水线fail,那么此时provider就会得知他们需要根据新的契约来修改实现了。
而和消费者驱动相反,提供者驱动的设计则是当provider发布了一个新的契约之后consumer侧的流水线会变红,直到consumer将他们的代码根据新的契约修正后才可以进入后面的集成测试。
1. 测试的速度快,无需依赖多个系统之间的交互
细心的同学通过上面的描述会发现,在契约测试时服务的依赖方式不需要被真实调用的,契约测试通过mock依赖的方式来模拟依赖方的行为,这就使得测试的速度得以大大提升
2. 可以并行开发
由于mock的存在,使得服务的消费方和提供方可以根据事先定义好的契约进行并行开发3. 发现问题后可以快速定位到问题:
因为问题只会出现在当前测试的服务或者组件中,你甚至可以确切的知道是哪个api测试fail了
4. 在确定完契约之后,开发人员可以在本地就可以进行测试,无需将代码推至远端
5. 测试前移
把本来要通过集成测试才能验证的工作化作单元测试和接口测试,用更轻量的方式快速进行验证,更早的发现问题使得后续的测试更加快速
总体来说,契约测试是一个介于单元测试和集成测试的一个阶段,他关注的细粒度比单元测试更粗,但是又无法取代集成测试。尤其是当你的产品对环境依赖特别大的时候,集成测试还是必不可少的一部分,契约测试的存在只是为了让你在开发过程中的联调更加快速,集成时问题更少。
最后希望这篇文章能够给你带来些许的启发~