0%

契约测试之核心解惑

在之前写的《契约测试之Pact By Example》中,我曾提到会再写一篇文章,来聊聊如何正确地认识和理解契约测试(好吧,至少是我认为的”正确地”)。但在随后的一年多时间里,对契约测试的讨论渐渐淡出了我的视野。我的理解是,随着微服务的大行其道,契约测试作为带刀护卫,已经深入人心了,所以没必要再去炒这碗冷饭,就像现在已经没有谁会再来码字吹Selenium一样(…请相信,我一定不是因为懒才这么说的o(* ̄3 ̄)o)。

然而,在最近参加的一次面向Dev的后端分享的讨论中,我意外的发现,契约测试作为构建微服务重要的一环工程实践,虽然确实已经被团队原生接受,但对于契约测试的理解,还存在一些认识上的盲点,特别是当契约测试与集成测试、接口测试一起讨论的时候,理解的偏差往往会被放大不少。所以,我想必要的码点字,分享一下我对契约测试的理解,还是有益的。


“契约测试,是建立在服务的消费者和生产者之间的……”(此处省略废话N多字),如果您要继续看下去,请注意:

  • 以下的内容不会涉及基本的契约测试概念,比如消费者、生产者、契约、消费者驱动等等,如果您对这些基本概念还不是很清楚,建议您可以花点儿时间先google一下,当然,Pact的官方文档可以是一个很好的开始;
  • 以下的内容不会涉及具体的契约测试编写和执行步骤,相关的内容,您可以参看我之前的文章《契约测试之Pact By Example》;
  • 如果您之前在任何地方、通过任何方式,看到过一些我对契约测试的观点的分享,并且觉得我就是在胡说八道,那您也不用看下去了,因为后面都是胡说十六道,而已;

关于测试的表述

在聊契约测试之前,让我们先来说一些平时看似毫不起眼的小话题—“测试的表述”。

“我们可以在E2E测试中覆盖这个场景,而不是单元测试…”

或者

“你们的E2E测试是怎么做的?…”

这里的E2E测试可能经常出现在我们的日常交流中,那你知道它的准确含义吗?答案是没有含义!它基本等价于你们一伙人去食堂吃饭(…笑啥,俺就是食堂党,咋的!),A:”今天吃啥?”,B:”新鲜的”。新鲜的啥?炒饭?面条?饺子?套餐?……

E2E,End To End,端到端,字面意思简单明了,但它只是一个副词(组),而不是一种测试类型。所以,我们真正想表述的,可能是E2E API Test。那么”E2E API Test”就完整的表述了一项测试活动了吗?不是的!E2E表示的是测试方式,API表示的是被测对象,但这里,我们还缺少被测对象的被测属性,比如,Function、Performance, Security等等,所以,一个比较完整的表述,往往可以是这样的:

test-expression

当然,平常的交流中,一般不会这么文绉绉地去抠字眼,因为我们彼此都清楚讨论问题的上下文,这点很重要。特别是针对E2E测试这样的表述。比如,我们有一个前后端分离、后端是微服务集群的系统应用,同样的E2E测试可能就代表着完全不同的测试活动:

e2e-context

如果从更多的维度来思考,比如套上测试四象限的模式,那么对于测试活动的表述,还会有更多考量。但今天的主题是关于契约测试的,所以就不过多的展开了。为什么要在讨论契约测试之前来废话”测试表述”呢?因为契约测试其实是多种测试方式的和思维的复合产物,比如,契约测试是E2E的测试吗?还是说是基于Mock的?契约测试是服务的接口测试还是集成测试?等等。所以,如果对这些基本的测试概念不是很清楚的,很容易迷失在契约测试的理念中。

为什么要做契约测试?

为什么要做契约测试?”因为我们是微服务”?(╬ ̄皿 ̄)=○

很多回答这个问题的答案,都关注在契约测试的目的上。那么,什么是契约测试的目的呢?简单来说,契约测试就是为了发现契约破坏(Contract Breaking)而进行的测试活动。如果你使用过Pact或者Spring Cloud Contract,你会发现,契约测试本身也是通过调用Provider的API接口来获取Response,再与契约文件中期望的结果做对比,从而验证契约是否正确。形式上,这和我们的API接口测试,或者针对功能的集成测试(以下简称集成测试,因为我们这里不讨论API的安全、性能等问题)是非常类似的。换句话说,我们通过API的接口测试或者集成测试,也能达到检查契约的目的,那为什么还要做契约测试呢?这种思考逻辑是完全正确的,也是为什么很多初学者都认为契约测试没有必要的原因。

那再问,为什么我们还要做契约测试呢?真正能够回答这个问题的,不是契约测试的目的,而是契约测试可以带来的价值!

契约测试的价值

那什么是契约测试的价值呢?要说清楚契约测试的价值,就需要准确认识契约测试的精髓–”消费者驱动”。

消费者驱动的字面含义,大家都清楚,但往往容易忽略的是被驱动的对象。在讨论契约测试的范畴里,”消费者驱动”述及的对象是契约,而不是契约测试。

当某个provider正常上线后,某个consumer需要消费这个provider的服务,那么应该由consumer来提出期望建立它们之间的契约测试。因为,契约测试,形式上,虽然测试的是provider,但,价值上,保证的却是consumer的业务。如果consumer对自己都不上心,你还期望provider来时刻关注你的死活吗?别笑,在跨团队的微服务体系下,这些都是真切的痛点。

理清了消费者驱动,就让我们来看看契约测试真正的价值吧。一个经典的案例:

multi-consumers-1

在上图一个简单的消费关系中,provider为consumer A,B,C提供服务。provider自己提供的schema包含name,agegender三个简单的字段。请注意,这份包含name,age和gender的JSON,其本身,只是一个schema,并不是任何契约。契约一定是成对存在的,没有确切consumer的交互定义,只是schema,不是契约。一个列子,中介打印了一份合同,上面写好了房屋租赁的全部信息,但在房东和租客都签字之前,这份”合同”并不具有任何效力,所以它根本就不是一份有意义的合同,法律上,它叫”要约”。(…感谢我大学的法律老师,我居然还记得这个词儿)

现在,这里有三份契约(对应的,就应该有三份契约测试),consumer A消费provider的age和gender,consumer B消费name、age和gender,consumer C消费name和gender。就目前provider提供的schema来说,没有任何问题,大家相安无事。

某日,因为业务需求,consumer C期望provider提供更加详细的name信息,包括firstName和lastName。这个需求对provider并不困难,所以,provider打算对schema做类似下面的修改。

multi-consumer-2

这样的修改,很明显,对consumer C是需要的,对consumer A无所谓,但对consumer B却是不可接受的,属于典型的契约破坏。此时,provider和consumer B之间的契约测试就会挂掉,从而对provider提出预警(至于,剩下的,怎么协调和consumer B的兼容问题,就不是契约测试关注的问题,那需要的是团队间的communication)。

上面这个示例中的一些细节,可以帮助我们发掘契约测试的价值点:

“consumer A没有使用name,consumer C没有使用age”,

基于消费者驱动的契约测试,契约的内容由consumer提供,其内容体现的是各个consumer对provider提供的schema的消费需求。这里的需求,不光包含consumer”需要什么”,还包含consumer”不需要什么”。这是非常有意义的,因为当你发现provider提供的schema的某些部分不被任何consumer消费时,就代表provider可以对schema的这些内容做任意的修改,完全不必担心会影响到任何consumer。这是契约测试非常重要的价值点。

“单个provider多个consumer”,

要最大化的体现契约测试异于集成测试的价值,一定是在”单个provider对应多个consumer”的架构下来说的。因为,在只有一个provider和一个consumer的架构下,只存在一份契约,对该契约内容的任何修改,对这对provider和consumer来说,都是显而易见的,那么就不会出现契约破坏的情况。说人话,就是,如果是consumer提出要修改契约,consumer一定知道改怎么消费新的契约内容;如果是provider提出修改契约,对于唯一的一个consumer,provider能很方便的告知其将要对契约的修改。并且,在这种情况下,集成测试往往就已经完整的达到了契约测试的目的。

而在单个provider对应多个consumer的架构下,情况就大不一样了。provider和consumer C之间的契约修改,对consumer A无感,对consumer B却是契约破坏,对此,集成测试是无能为力的。仔细来看,这里有4个service,就会有4个集成测试。但每个集成测试都只会关注自己的业务正确性,具体来说:

  • consumer A,因为不受影响,所以A的集成测试没有任何变化;

  • consumer C,因为是契约修改的提出者,所以它会在provider提供新的schema后修改自己的集成测试,没有问题;

  • provider,如果接受了consumer C的需求,大摇大摆地修改了schema,它也会相应的修改自己的集成测试,因为对provider来说,这个变更是正常的业务需求,也没有问题;

  • consumer B,最倒霉,啥都没干就挂了,当然,它的集成测试会捕捉到这个failure,但那都是在provider的契约破坏生效之后的事情了,能做的也只有亡羊补牢。

可见,虽然4个集成测试都各司其职,但都不能对这个契约破坏的问题做到防患于未然!只有契约测试,才是这个问题的最佳答案!这就是契约测试最大的价值,它只会在”单provider多consumer”的环境下(这是微服务的常见场景,但不是必然场景),才能发挥出来。

“很显然,对consumer A无害,但对consumer B却是契约破坏”,

"很显然",仅仅是对于我们这个简单得不能再简单的示例而言,真正的业务场景下,特别是一些复杂的微服务集群,又或者是一些时间跨度很长的系统,对于某个provider,到底有多少个consumer?而provider的每一处修改,又到底会对哪些consumer的契约造成怎样的影响?这些往往都是很难确定的问题。我最近所在的一个集团项目上,一个搜索地址的基础服务provider,有十个左右的consumer,其中有八个consumer没有契约测试,就不清楚它们对provider的API具体是如何消费的,所以每次provider要更新,就得八方去通知这些consumer的团队来做回归测试。有时,一点小小的修改,回归测试一分钟就可以搞定,但人肉联系各个团队却会花上好几天……

如果每个consumer都能和provider建立契约测试(这里我们暂且不考虑负载和去重的问题),通过类似Pact Broker这样的实践,我们就能很好的解决这些效率问题。

contracts

OK,理解透契约测试的这些价值后,对于”要不要做契约测试?”、”谁来做契约测试?”这些问题,相信你就不再疑惑了。想再次强调一下的是,契约测试很多情况下基于微服务而生,但并不代表每个微服务都一定需要契约测试。相对的,一些传统的单体服务,它的架构设计和部署实施,完全和微服务的理念相反,但它提供的服务却被众多的下游消费者使用,那么这样的服务,也有很强的契约测试需求。所以,千万不要把契约测试和微服务做”死绑定”,一定要基于服务的业务来考虑策略。

契约测试和接口测试、集成测试的区别

“契约测试和接口测试、集成测试的区别”,从2015年我第一次在BQConf讲契约测试,到写这篇文章之前,最近一次和别人讨论契约测试,这都是一个一直被提起的问题。在上面的内容中,其实已经或多或少的提到了相关的内容。由于具体的测试方式,都是”调用API验证Response”,契约测试、接口测试、集成测试经常被放在一起来进行比较,甚至质疑彼此。

先让我们来看看接口测试和集成测试。说实话,对于测试理论夯实的QA来说,这里应该没有任何问题的,因为接口测试和集成测试,它们压根儿就是从完全不同的维度来描述测试活动的。

前面说过,如果要完整的描述一个测试活动,至少需要考虑三个内容:测试方式、被测对象、被测属性。然而,”接口测试”和”集成测试”,显然,都是我们根据上下文使用的简称,更准确的:

测试方式 被测对象 被测属性
接口测试 调用API接口 只能是API
集成测试 肯定是被测对象在于外部依赖集成时的行为表现

接口测试

  • 被测属性 — 不定,可以是被测对象的性能或安全行为,但根据上下文,默认是功能行为;

集成测试

  • 测试方式 — 不定,可以直接进行E2E的测试,也可以进行基于Mock的测试;
  • 被测对象 — 不定,可以是UI,也可以是API,但根据上下文,默认是API;

所以,基于不同的维度,我们有”接口测试”和”集成测试”的表述,但,当放在和契约测试来讨论的时候,它们描述的可能是同样的测试活动。即,通过调用API接口,来测试API的功能行为。

这里,想强调一下集成测试中的”集成”。对于传统的瀑布开发模式,对应的测试流程按照测试级别(Test Level)划分,一般是:单元测试 -> 集成测试 -> 系统测试 -> 验收测试,这是”集成测试”早期的由来。

那会儿的应用,往往是庞大的单体服务,服务内部有分工明细、边界分明的”模块”。这些模块被并行开发,就绪后就会进行彼此集成。集成的对象,一般可以简单分为:逻辑模块、数据库模块、外部服务模块。比如,在上古时代,对数据库的操作是比较繁琐的,开发人员往往需要自己组装SQL语句,然后封装成模块来供上层调用。单元测试可以保证这些模块自己的逻辑正确,但像”模块中的各个函数接受的参数个数和参数类型是否和模块使用者的需求相匹配”这样的问题,就需要集成测试来确保(集成不等于集成测试,内容所限,我就不过多说明了)。这些测试都是发生在单体服务内部的,类似于现在的组件测试。

如今,微服务的设计,将不同业务的”模块”拆分成了不同的服务,各个服务都是高内聚的。以Spring为例,Controller -> Service -> Repository,内部垂直划分,简单明了。像上面提到的手写SQL这样的数据持久化工作,已经基本不存在了,取而代之的是像spring-boot-starter-data-jpa或spring-boot-starter-data-mongodb这样功能强大、方便易用的公共组件,最重要的,这样的公共组件,一般都有很高的官方质量保证的。所以,结论就是,在上古时代的那种传统的集成测试,在微服务的体系下,已经基本不需要了。

而对于单个微服务的质量保障,特别是当这个微服务有外部集成的时候,比如数据库或者外部服务,我们仍然需要进行检查外部集成的测试。再结合微服务业务的单一性,我们可以很自然的将这种”检查外部集成的测试”合并到API的接口功能测试中。说人话就是,对于微服务,只进行API的接口功能测试,既涵盖对被测服务领域逻辑的检查,又覆盖其对外部集成的检查。

当然,这里已经讨论到了微服务测试策略了,我就不再过多展开了。话收回来,如果要和契约测试进行区别比较的话,我们只用考虑功能性的API接口测试就可以了。

理清了接口测试和集成测试的内部姻缘(下面我统称功能测试),我们就最后来说说它们和契约测试的区别吧~
其实,上面那个示例,已经很好的展现了它们的区别,我就不过多解释了,简单来说:

  • 功能测试关注的是provider的实现正确体现其设计,契约测试关注的是provider的实现(当然,肯定也包括设计)满足每一个consumer的需求。注意,功能测试只关注provider自身,契约测试关注每一个consumer;

  • 功能测试的测试案例,由provider的团队提供,契约测试的测试案例,基于消费者驱动,由各个consumer团队提供;

  • 一个provider只会有一个功能测试(谁要纠结”一个功能测试”是几个testcase,就把TA拖出去枪毙三分钟),但契约测试,理论上,可以无限,有多少consumer就可以有多少个契约测试;

  • 同样的一个testcase,在功能测试里面出现一次,在契约测试里面出现N次,它们的含义是完全不同的。什么含义,自己琢磨琢磨;

  • 一个testcase,出现在功能测试里面,却没有出现在契约测试里面,是非常有意义的。啥意义,再自己琢磨琢磨;

  • 功能测试可以自娱自乐,契约测试必须组”对”上分;

契约测试可以替代集成测试吗?

“契约测试替代集成测试”,说实话,第一次听见这个说法的时候,我是非常惊讶的,这得多大的脑洞才能给出这样的命题呀!

提示一下,就题论题,这里的”集成测试”,并不全等与上面提到的”功能测试”,仅仅是一般论的集成测试。

先来揣测一下,为什么会有这样的问题吧。我们知道,在Pact(JVM)的实施过程中,第一步是在consumer端生成契约文件。这期间,Pact会根据自定义的契约,在consumer端启动一个mock server(如果你有看源码,就知道它只是一个普通的HttpServer实例),consumer向这个mock server发送request获取response,整个过程被记录成JSON的契约文件。

这个流程的最后一步,一直有一个大家乐于争论的话题:”要不要对response的内容做断言检查?”。这是一个很开放的问题,没有标准的答案。但我想强调的是,不加断言,这一切只是一个”流程”或者说”步骤”,加上断言,它就是测试。是的,对consumer来说,它就是consumer的一种集成测试(啥?”用的是Mock Server,都没有集成真正的provider,为什么叫集成测试?” 如果你有这个问题,可以再仔细想想集成测试的真正含义……)。

以上是解题背景。现在,让我们再来省一下题吧,”契约测试可以替代集成测试吗?”,这里,其实隐藏了很大的一个意识盲点。契约测试,描述的测试活动,一定是架设在一对consumer和provider之间的。那么题目里的集成测试呢?你是想替换consumer端的集成测试?还是想替换provider端的集成测试?还是说其实你也不清楚到我想替换哪一端的集成测试……”不!我想说的不是两个服务之间的集成的那种测试,而是整个系统,包括全部上下游服务,集成在一起的集成测试”……诶,好吧,那叫系统(E2E)测试……

还是让我们回到一般论的集成测试上来吧(不然,要说的实在太多了 T_T),无论是consumer端还是provider端,集成测试的关注点,是consumer是否可以正确的消费provider的API,这里的”消费”包括调用接口和解析数据。它的被测对象,注意,一定是consumer,或者说,是一个服务作为consumer的角色(因为,某个服务经常既是consumer,又是provider)。而契约测试的被测对象,一定是provider。好了,这就是问题的核心,其它的细节,我想就不必再赘述了吧。

关于Pact和Spring Cloud Contract

“用Pact还是Spring Cloud Contract?”,这是另一个经常被讨论的话题。它背后折射的却是另一个非常重要的概念博弈:契约测试 vs 基于契约的测试(契约驱动的测试)

Pact的理念是消费者驱动的契约测试。什么是契约测试呢?目前,我没有找到任何”权威”的定义。其实,面向工程实践的理念,也许根本就没有权威,有的只是最适用于自身的实践总结。即便如此,我还是希望以个人的视角,提供一些解读:

  • 如果你google搜索contract test,你得到的第一个答案肯定是Martin Fowler在2011年的这篇文章,但遗憾的是,老马这里讨论的契约测试,是解决在集成测试中,如何保证测试替身有效性的问题的,它和我们今天讨论的契约测试并不是一回事。但是,如果抛开契约测试的内容,而单论”契约测试”的定义的话,老马的文章其实表述了一个很有价值的点,那就是”契约是需要测试的”,这是非常有意义的。

  • Pact的官方文档,是另一个可以帮助我们理解契约测试的地方。它对契约测试给出了这样的定义:Contract testing is a way to ensure that services (such as an API provider and a client) can communicate with each other.,这里面需要关注的重点是communicate,它给出了Pact对契约测试范畴(scope)的定义。

  • 对于任何以”XXX测试”命名的测试活动,我们都遵循同样的一个理解的公理:”XXX”一定是被测对象或被测属性。比如,UI测试,测试对象一定是UI;安全测试,测试的一定是被测对象的安全表现;兼容性测试,关注的一定是被测对象在兼容性方面的问题,等等。同样的,”契约测试”,被测对象一定是服务之间的契约。

好了,有了这三点重要的理论基础,就让我们来具体看看Pact和Spring Cloud Contract(以下简称SCC)的区别吧。

pact-springCloudContract

在上面的图中,给出了Pact和SCC具体的使用方式(逻辑路径)。当然,如果你有一些基本的Pact或SCC的使用经验,就再好不过了。

Pact,在consumer端生成契约文件,发布到Pact Broker,而后,provider从Pact Broker获取契约文件,触发provider端执行契约测试。

SCC,实际生成契约文件的工作是发生在provider端的,基于这份契约文件,在provider端,生成了Java的测试案例,这些测试案例用于provider的功能测试;而在Consumer端,使用同一份契约文件作为Stub,生成了基于WireMock的mock service,consumer可以使用该mock service来做集成测试。

可见,Pact作为消费者驱动契约测试的倡导者,真正地实践了消费者驱动的契约测试。相对的,SCC,既没有实际的将契约作为被测对象来进行测试,更没有确实地实现”消费者驱动”。SCC的做法,实际上是基于同一份契约,分别驱动了consumer端的集成测试和provider端的功能测试。所以,Pact和SCC的区别,就在于,前者做的是”契约测试”,后者做的是”基于契约的测试(契约驱动的测试)”。

如果有同学阅读过SCC的文档,一定会质疑,SCC明文写着”Spring Cloud Contract Verifier enables Consumer Driven Contract (CDC) development of JVM-based applications”,那为什么说它没有确实地实现”消费者驱动”呢?因为在SCC的设计中,原始契约文件是在provider端生成的。为了实现CDC,consumer需要在其本地克隆provider的代码仓库,”借”provider来生成原始的契约文件。显然,在现实的项目中,consumer团队不可能随心所欲的获取到provider代码仓库访问权限,所以有了后来的,基于Share Repo的解决方案,来实现契约的共享(编辑和使用)。所以说,从最初的设计思想来看,SCC并没有像Pact那样,”实实在在”地实践了消费者驱动的契约测试。

那么,到底是选择Pact(契约测试)还是SCC(基于契约的测试)呢?答案是”按需取舍”。
比较Pact和SCC的目的,并不是区别彼此的好坏长短,而是阐述它们各自不同的测试理念。Pact的价值点,前面已经说过了,SCC,虽然做的并不是真正的契约测试,但它通过共享(同一份)契约的方式,实现了微服务测试中,consumer和provider之间E2E集成测试的解耦,这在实际项目中,也是有重要的现实意义的。感兴趣的同学可以自己下来多研究研究,我就不在这里扩展了。

一些问题

至此,在我看来,契约测试相关的认识难点,就已经基本解读到了。但在结束全文之前,有两个问题,我还想再阐述一下:

consumer端的集成测试需要做到什么程度?

对于Pact,前面提到,在consumer端生成契约文件的时候,加上断言语句后,就”构成”了consumer端的集成测试。这个集成测试,从Pact的角度来说,是可选的,它的目的是保证consumer端生成的契约文件本身是正确的。但从consumer的角度来说,要不要进行这一层级的集成测试,取决于consumer团队自己的测试策略。我想说的是,如果要进行这一层级的集成测试,请一定合理把握你的测试粒度和测试范畴。

  • 测试粒度,由于这里的集成测试是和契约测试强绑定的,如果为了增加集成测试的覆盖率而设定过小的测试粒度,会大大增加契约测试的测试案例。而其中的一些测试案例,对于关注功能的集成测试来说,可能是不同的等价类,但对关注schema的契约测试来说,则完全可能是相同的等价类,从而造成测试冗余。所以,合理的把握测试粒度,是非常重要的。当然,就个人意见,我是反对这种和契约测试绑定的集成测试的。功能测试和契约测试,是完全不同的测试活动,它们肩负各自的使命、体现各自的价值,应该各司其职。这是我和Beth Skurrie(Pact最主要的核心开发成员,没有之一)多次探讨的一致共识。

  • 测试范畴,是另一个需要考虑的问题。上面提到过,Pact将契约测试的范畴定义在了communicate。什么是communicate呢?很简单,通过通讯获取信息。具体到契约测试中,(可)通讯,体现在API的endpoint接受request(request包括protocol,url,header,body等),返回response;(可)获取信息,体现在获取的response能够被按照期望的方式解析(反序列化)。需要强调的是,communicate的内容不应该包含”使用信息”。使用信息,是consumer的领域逻辑需要处理的问题,而信息使用得是否正确,则应该是consumer的功能测试关注的范畴。注意,这里的功能测试可以发生在单元测试、组件测试、集成测试等各个测试级别。这就是为什么Pact的官方示例文档中,在consumer端,仅仅断言了response的status code这些非常简单的数据。如果consumer团队确实有需求,跨出communicate的范畴来构建集成测试,那么请一定合理斟酌你们的测试范畴。

test-scope

“生产者驱动的契约测试”?

相较于到目前为止通篇强调的”消费者驱动的契约测试”,你可能在其他地方,或多或少的,看到过”生产者驱动的契约测试”的命题。
单论契约,确实可以分为”消费者驱动的契约”和”生产者驱动的契约”,但述及契约测试,到目前为止,恕俺视野有限,我并不认为”生产者驱动的契约测试”是一种正确的表述。

  • 契约不等于契约测试,这不必赘述;

  • 无论是消费者驱动、还是生产者驱动,其实质一定都必须是契约测试。这点,消费者驱动的契约测试不必多说,但对于”生产者驱动的契约测试”,事实可能并不是这样。生产者驱动的契约测试,其实质,就是上面讨论过的基于契约的测试(契约驱动的测试)

  • 具体来说,生产者驱动的契约测试,强调的是,当provider有需求和计划更新既有服务的schema时,在实际部署变更之前,先更新相应的”契约”(为什么这里的契约要加引号,自己琢磨琢磨),新的”契约”,如果包含契约破坏,会导致consumer端的(契约驱动的集成)测试挂掉。由此,consumer端可以在provider端真正部署包含契约破坏的服务之前,获得预警,从而对consumer做必要的更新准备,来适配provider将会部署上线的更新内容;

  • 在我看来,这是契约测试的一种反模式。在消费者驱动的契约测试中,契约是复数存在的,每一份契约都会被provider测试,如果有契约破坏,会被及时反馈,必要的时候会被修正;而”生产者驱动的契约测试”中,”契约”是唯一存在的,它的正确性是不会被测试和质疑的,它仅仅会被consumer用来验证自己能否正确消费这份”契约”,所以,”生产者驱动的契约测试”,测试的并不是”契约”,而是consumer。

  • “如果质疑生产者驱动的契约测试,是因为它测试的不是契约,而是consumer,那么是否也可以质疑消费者驱动的契约测试,测试的也不是契约,而是provider呢?” 形式上来看,好像确实如此。但如果我们进一步分析,不难发现,消费者驱动的契约测试,对于不可接受的契约破坏的最终结果,要么是provider自主的功能修改被驳回,要么就是consumer主张的契约变更被驳回。结论就是,消费者驱动的契约测试,是对契约的双方进行约束,这体现了契约的意义,另一方面,对于不可接受的契约破坏,无论是哪一方引入的,它都将会被驳回,这体现了测试的意义(任何“功能”,如果交付测试后,无论结果好坏,它都是不可逆的,那测试本身也就失去了意义)。再来看“生产者驱动的契约测试”,一旦provider发布了“契约”,无论是否发生(对任一consumer)不可接受的契约破坏,无论“测试”的结果如何,这份“契约”都不可能被驳回,这样的“测试”,如果还说它的测试对象是“契约”的话,那这种“测试”对契约来说是没有意义的。归根到底,还是”契约测试”和”基于契约的测试(契约驱动的测试)”的区别。

  • 当然,这样的测试活动,并不是一无是处,在一些上下游非常不稳定的微服务集群中,特别是在一些服务集群跨部门,甚至跨公司的多团队合作项目中,由于缺乏及时有效的沟通,往往更容易造成这样那样的契约破坏,此时,这种基于契约的测试活动,能很好的预警provider的API schema变更对consumer的影响,这是非常有意义的。

最后

关于契约测试本身,和契约测试实施的问题,我想,远不止上面诉及的方面。不同的人、不同的团队,对契约测试的理解也可能都不一样,特别是,当一种(比较)新的理念在不同的现实项目中付诸实践时,可能遇到的问题,和思考的方式又会有所迥异,这些都是我们理解一种理念的正常途径。

问题永远都是客观存在的,但解题的思路却可以千奇百怪。我们讨论Pact、Swagger和Spring Cloud Contract,我们辩驳消费者驱动和生产者驱动,我们思考是先写契约测试还是先写功能测试,这些思想的碰撞越多,越能帮助我们去思考、理解和总结,继而产生出更加富有想象力的答案。比如,当需要把provider的schema中的一个String改成Object,从契约的角度,我们还在纠结如何协调所有的consumer影响最小时,“聪明”的小伙伴已经给出了这样一个答案:不把String改成Object,而是直接添加这个Object

最后,送上我经常讲的一个问题:你知道最简单靠谱的契约测试工具是什么吗?是邮箱!╮( ̄▽ ̄)╭