SpringBoot中基于Pact的契约测试

背景

如今,契约测试已经逐渐成为测试圈中一个炙手可热的话题,特别是在微服务大行其道的行业背景下,越来越多的团队开始关注服务之间的契约及其契约测试。

什么是契约测试

    关于什么是契约测试这个问题,首先先看一下Pact官方文档给出的定义: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)的定义。契约测试又称之为 消费者驱动的契约测试。这里的契约是指软件系统中各个服务间交互的数据标准格式,更多的指消费端(client)和提供端(server)之间交互的数据接口的格式。

契约测试的价值是什么

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

在讨论契约测试的范畴里,”消费者驱动”述及的对象是契约,而不是契约测试。所以谁被驱动的对象就是契约。举个例子,当某个provider正常上线后,某个consumer需要消费这个provider的服务,那么应该由consumer来提出期望来建立它们之间的契约测试。因为,契约测试,形式上,虽然测试的是provider,但是在价值上,保证的却是consumer的业务。如果消费者对自己都不上心, 那你也不要指望生产者能操什么心。这些都是在跨团队的微服务体系下真切的痛点。 这里举一个契约测试的经典:

在上图一个简单的消费关系中,provider为consumer A,B,C提供服务。provider提供的结构包含name、age和gende三个简单的字段。这份包含name、age和gender的JSON,其本身只是一个schema,并不是任何契约。因为契约一定是成对存在的,没有确切consumer的交互定义,只是schema,不是契约。

    如上图有三个消费者,并且消费的字段各不相同,所以这里需要有三份契约(对应的,也需要三份契约测试)。

  • consumer A消费page和gender,
  • consumer B消费name、age和gender;
  • consumer C消费name和gender

就目前provider提供的schema来说,没有任何问题,大家相安无事。但是某日因为业务需求,consumer C期望provider提供更加详细的name信息,包括firstName和lastName。这个需求对provider是小case,所以,provider打算对schema做类似下面的修改:

  这样的修改,很明显对consumer C是需要的,对consumer A无所谓,但对consumer B却是不可接受的,属于典型的契约破坏。此时,provider和consumer B之间的契约测试就会挂掉,从而对provider提出预警(至于,剩下的,怎么协调和consumer B的兼容问题,就不是契约测试关注的问题,那需要的是团队间的交流)。上面这个示例中的一些细节,可以帮助我们发掘契约测试的价值点

1. 应对单个provicder多个consumer

要最大化的体现契约测试异于集成测试的价值,一定是在"单个provider对应多个consumer"的架构下来说的。因为,在只有一个provider和一个consumer的架构下,只存在一份契约,对该契约内容的任何修改,对这对provider和consumer来说,都是显而易见的,那么就不会出现契约破坏的情况。在这种情况下,集成测试往往就已经完整的达到了契约测试的目的。

  但是在单个provider对应多个consumer的架构下,情况就不一样了在上文的例子中provider和consumer C之间的契约修改,对consumer A无影响,对consumer B却是契约破坏,对这种情况,集成测试是无能为力的。在上边例子中,有4个service,所以就会有4个集成测试,每个集成测试只会关注自己的业务正确性,provider修改后,只有consumer B的集成测试会挂掉。但那都是在provider的契约破坏生效之后的事情了。可见,虽然4个集成测试都各司其职,但都不能对这个契约破坏的问题做到防患于未然!只有契约测试,才是这个问题的最佳答案!这就是契约测试最大的价值,它只会在"单provider多consumer"的环境下(这是微服务的常见场景,但不是必然场景),才能发挥出来。

2.减少团队沟通成本  

  真正的业务场景下,特别是一些复杂的微服务集群,又或者是一些时间跨度很长的系统,对于某个provider,到底有多少个consumer?而provider的每一处修改,又对哪些consumer的契约造成怎样的影响?这些往往都是很难确定的问题。当在集团业务中一个provider有十几个 consumer时,每次provider要更新,就得八方去通知这些consumer的团队来做回归测试。有时,一点小小的修改,回归测试一分钟就可以搞定,但人肉联系各个团队却会花上好几天。如果每个consumer都能和provider建立契约测试(这里我们暂且不考虑负载和去重的问题),我们就能很好的解决这些效率问题。

契约测试和功能测试区别

首先这里的功能测试是指接口测试和集成测试, 学习契约测试的时候一定要弄清楚契约测试和功能测试)之间的区别。契约测试主要是用于以下几点

  • 测试接口和接口之间的正确性
  • 验证服务层提供的数据是否是消费端所需要的
  • 将本来需要在集成测试中体现的问题前移,更早的发现问题
  • 更快速的验证消费端和提供端之间交互的基本正确性

根据契约测试的用途我们可以发现契约测试和功能测试之间的区别如下:

1. 功能测试关注的是provider的正实现其设计,契约测试关注的是provider的实现(也包括设计)是否满足每一个consumer的需求。注意,功能测试只关注provider自身,而契约测试则关注每一个co;

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

3. 一个provider只会有一个功能测试,但契约测试,理论上,可以无限,有多少consumer就可以有多少个契约测试;

4. 集成测试的测试对象是一定是consumer,或者说是一个服务作为consumer的角色(因为,某个服务经常既是consumer,又是provider,而契约测试的被测试对象一定是provider;

Example by Pact

Pact最早是用Ruby实现的,目前已经扩展支撑Java,.NET,Javascript,Go,Swift,Python和PHP。 这里我使用springboot+PACT+gradle搭建契约测试。

1.添加依赖

  在项目的build.gradle文件中添加如下依赖

buildscript {
  ext {
        pactVersion = "4.0.2"
        kotlin_version=1.3.50
    }
    dependencies {
        classpath("au.com.dius:pact-jvm-provider-gradle:${pactVersion}")
    }
}

apply plugin: "au.com.dius.pact"
dependencies {
    testImplementation "au.com.dius:pact-jvm-consumer-junit:${pactVersion}"
    testImplementation "au.com.dius:pact-jvm-consumer-java8:${pactVersion}"
}

这里有几个注意点:

1. 由于这里用的pact的版本是4.0.2的,pact插件中依赖的kotlin版本是1.3.50,所以项目中kt的版本也要是1.3.50,然而springboot现在默认自己管理kt的版本,目前项目中采用的springboot是2.1.10.RELEASE默认使用的kt是1.2.7,会导致pact加载失败,所以要自己手动指定kotlin版本。

2.有写时候引入的依赖包中可能会指定pact版本,而且是低版本的这个时候也要注意版本冲突。

2.编写测试用例

想编写一套完整的PACT测试用例一般分为以下四步

1.确定consumer需求

契约测试编写测试用例,首先要素是根据consumer需求编写,这里我假设consumer需求如下

  • Get请求:http://ip:port/ic/{id}?type=test
  • 要求返回的响应

    {
      "id": "a7a1b044-b8a8-4ef9-ae1b-00599f2281cc",
      "data": {
        "type": "test",
        "projectName": "testadmin",
        "machineCode": "1111",
        "validity": "2022-01-18 23:59:59.0",
           "useNum":500
      }
    }

2.编写consumer测试代码

  根据consumer的需求编写Test case

@Test
  public void testWithQuery() {

    // 构造consumer血药验证的响应内容
    DslPart body = newJsonBody((root) -> {
      root.stringType("id");
      // 对应上文的data结构
      root.object("data", (dataObject) -> {
        // 验证返回的type的值是否是"test"
        dataObject.stringValue("type", "test");
        //验证类型是否为string
        dataObject.stringType("projectName", "tesadmint");
        dataObject.stringType("machineCode");
        dataObject.timestamp("validity");
        dataObject.numberType("userNum", 500);
      });
    }).build();

     RequestResponsePact pact = buildPactResponse("test",5,body);

    MockProviderConfig config = MockProviderConfig.createDefault(PactSpecVersion.V3);
    PactVerificationResult result = runConsumerTest(pact, config, (mockServer, context) -> {
      // 自己的客户端调用服务
      TestRestService testRestService = new TestRestService ();
        // 生成一个mockServer,代替服务端返回响应
         TestResponse testResponse= TestResponse
          .fetchLicenseGetId(mockServer.getUrl(), "/lic/1?licenseType=test");
     // 返回的响应内容
      TestData testdata= testResponse.getData();
      // 验证响应内容
      assertEquals(testdata.getType(), "test");
      return null;
    });
    checkResult(result);
  }

//  返回响应的请求头类型
  private RequestResponsePact buildPactResponse(String licType, int id,DslPart body) {
    Map<String, String> headers = new HashMap<String, String>();
    headers.put("Content-Type", "application/json;charset=UTF-8");
    return ConsumerPactBuilder
        .consumer("SignLicGetIdConsumer")
        .hasPactWith("SignLicProvider")
        .given("")
        .uponReceiving("Query " + licType + " lic is " + id)
        .matchPath("/lic/[0-9]+", "/lic/" + id)
        .query("licType=" + licType)
        .method("GET")
        .willRespondWith()
        .headers(headers)
        .status(200)
        .body(body)
        .toPact();
  }

其中TestRestService 是consumer应用代码中的类,我们直接使用它来发送真正的Request,发给谁呢?发给mockServer,Pact会启动一个mockServer, 基于Java原生的HttpServer封装,用来代替真正的Provider应答createPact中定义好的响应内容,继而模拟了整个契约的内容。

编写测试类我这里使用的是Junit DSL方式,这种方式可以在一个测试类中编写多个测试方法而基本的Junit和Junit Rule的写法只能在一个测试文件里面写一个Test Case。当然,Junit DSL的强大之处绝不仅仅是让你多写几个Test Case,通过使用PactDslJsonBody和Lambda DSL你可以更好的编写你的契约测试文件:

    • 对契约中Response Body的内容,使用JsonBody代替简单的字符串,可以让你的代码易读性更好;
    • JsonBody提供了强大的Check By Type和Check By Value的功能,让你可以控制对Provider的Response测试精度。比如,对于契约中的某个字段,你是要确保Provider的返回必须是具体某个数值(check by Value),还是只要数据类型相同就可以(check by type),比如都是String或者Int。你甚至可以直接使用正则表达式来做更加灵活的验证;
    • 目前支持的匹配验证方法请参考官方文档,这里不多说

3.设置契约生成目录

在build.gradle中添加契约文件存放地址

test {
    systemProperties[‘pact.rootDir‘] = "${buildDir}/Pacts/"
}

4. 运行测试类  

在junit中运行clean test,运行成功后会生成在对应目录下契约文件。这里我用的idea,所以直接运行gradle task即可

Provider测试

comsumer端测试代码编写完毕,契约也生成好了,接下来就是要执行Provider端测试了,要想执行Provider测试,首选要获取consumer端的契约文件;契约文件,也就是上文Pacts目录下面的那些JSON文件,可以用来驱动Provider端的契约测试。由于我们的示例把Consumer和Provider都放在了同一个codeBase下面,所以Pacts下面的契约文件对Provider是直接可见的,而真实的项目中,往往不是这样,你需要通过某种途径把契约文件从Consumer端发送给Provider端。Pact提供了更加优雅的方式那就是使用Pact Broker。目前有好些方法可以搭建Broker服务,我推荐使用Docker来个一键了事。

要想发布到Broker上,需要配置发布地址,在gradle中加入如下配置

pact {
    publish {        // 契约地址
        pactDirectory = "${buildDir}/${pactPath}/"       //broker url        pactBrokerUrl = mybrokerUrl
    }
}

这里搭建的broker不需要用户名和密码, 所有无需配置用户名和密码。配置完后运行发布任务pactpublish,如果是idea的话在右边是可以直接找到发布的task,双击便可执行

发布成功后在broker上可以看到consumer的契约文件。如下:

consumer发布契约成功后,provicer就可以从broker上拉取契约文件了,在build.gradle的pact Task中添加serviceProviders配置

pact {
    publish {
        pactDirectory = "${buildDir}/${pactPath}/"
        pactBrokerUrl = mybrokerUrl
    }
    serviceProviders {
        SignLicProvider {
            protocol = ‘http‘
            host = ‘localhost‘
            port = 8880
            path = ‘/‘

            // Test Pacts from local
            hasPactWith(‘‘) {
                pactSource = file("${buildDir}/${pactPath}/SignLicGetIdConsumer-SignLicProvider.json")
            }

            // Test Pacts from Pact Broker
            hasPactsFromPactBroker(mybrokerUrl)
        }
    }
}

如果想把provider端测试的结果提交到broker上,需要开启结果上传配置。 在build.gradl中 添加pact_verifier_publishResults=true即可。添加成功以后,就可以执行provider端的契约测试了。在执行provider端的测试之前,要先保证provider端的服务开启,否则无法工作。在idea中的task中可以找到新增的对应的task:pactVerify_SignLicProvider,然后双击运行。也可以手动执行task:SignLicProviderr:pactVerify。

task执行成功后控制台会显示契约测试是否执行通过,或者在broker上也可以看到最近提交的结果。

最后我们就可以根据契约测试的结果,进行沟通或者修改了。

总结

一般契来说约测试是在单元测试之后,集成测试之前要进行的,首先在保证各自功能正确的前提下测试消费者和提供者的契约是否相匹配,然后再进一步的测试功能的完备性和整个业务流的正确性。

从上文可以得出契约测试可以解决如下问题:

  1. 可以使得消费端和提供端之间测试解耦,不再需要客户端和服务端联调才能发现问题
  2. 完全由消费者驱动的方式,消费者需要什么数据,服务端就给什么样的数据,数据契约也是由消费者来定的
  3. 测试前移,越早的发现问题,保证后续测试的完整性
  4. 通过契约测试,团队能以一种离线的方式(不需要消费者、提供者同时在线),通过契约作为中间的标准,验证提供者提供的内容是否满足消费者的期望

参考链接

https://github.com/pact-foundation/pact_broker

https://github.com/DiUS/pact-jvm/tree/master/consumer/pact-jvm-consumer-junit

https://www.jianshu.com/p/ca82cde5b125

原文地址:https://www.cnblogs.com/NathanYang/p/12036744.html

时间: 2024-08-26 19:50:53

SpringBoot中基于Pact的契约测试的相关文章

契约测试(Pact)

为什么要使用契约测试(Pact) 目前开发过程中存在的问题 联调成本过高,要双方开发到某一阶段后放在同一个环境上才能进行,要同时把握双方的进度,造成资源和时间上的浪费. 对于接口的变动把控相当困难.由于接口变动是普遍存在的,尤其对于调用关系复杂的接口,一旦发生变动,如果没有一套机制进行控制,验证的成本巨大.更不必说持续集成了,只能成为空谈. 契约测试能给我们带来什么 通过使用契约测试,接口调用双方协商接口后就可以并行开发,并且在开发过程中就利用契约进行预集成测试,不用等到联调再来集成拉通接口,一

(001)springboot中测试的基础知识以及接口和Controller的测试

(一)springboot中测试的基础知识 (1)添加starter-test依赖,范围指定为test,只在执行测试时生效 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> 完整po

微服务测试之接口测试和契约测试

日常开发过程中,项目的接口通常由服务提供方约定和提供,微服务模式下接口被多个消费者调用更是常态,那么提供方接口的变更如何快速.高效.无遗漏的通知给消费者呢?另外,当一个service同时被多个使用者调用,如何保证对service的修改可以让其它所有使用者造成的影响都能被感知到?这些问题契约测试可以给你答案.另外,微服务模式下,接口测试是非常重要的测试手段,它在实际的项目中帮助验证微服务之间的协同和交互,大幅降低测试成本和提高测试效率方面提供了很大帮助,可以说接口测试是业务功能测试前置的助推器.因

SpringBoot中使用LoadTimeWeaving技术实现AOP功能

目录 1. 关于LoadTimeWeaving 1.1 LTW与不同的切面织入时机 1.2 JDK实现LTW的原理 1.3 如何在Spring中实现LTW 2. Springboot中使用LTW实现AOP的例子 3. 参考资料 1. 关于LoadTimeWeaving 1.1 LTW与不同的切面织入时机 AOP--面向切面编程,通过为目标类织入切面的方式,实现对目标类功能的增强.按切面被织如到目标类中的时间划分,主要有以下几种: 1.运行期织入 这是最常见的,比如在运行期通过为目标类生成动态代理

同一个Docker swarm集群中部署多版本的测试环境

先介绍下用到的技术 Docker swarm: Docker官方的集群管理工具,相比kubernetes更加简单,容易入门.https://docs.docker.com/engine/swarm/ Traefik: 一个现代化的反向代理工具,原生支持Docker swarm模式,可以实现swarm的动态代理.https://docs.traefik.io/user-guide/swarm-mode/ 下图展示主要的思路: 在Docker swarm中创建某个测试版本service时,通过设置s

基于端口的虚拟局域网测试

基于端口的虚拟局域网测试 一.什么是VLAN: VLAN,是英文Virtual Local Area Network的缩写,中文名为"虚拟局域网", VLAN是 一种将局域网(LAN)设备从逻辑上划分(注意,不是从物理上划分)成一个个网段(或者 说是更小的局域网LAN),从而实现虚拟工作组(单元)的数据交换技术. VLAN 主要用来解决如何将大型网络划分成多个小网络,隔离原本在同一个物理LAN中的不同主机间的二层通信: 在LAN中,各主机之间的通信是物理通信(二层通信): 在VLAN中

SpringBoot中使用Spring Data Jpa 实现简单的动态查询的两种方法

首先谢谢大佬的简书文章:http://www.jianshu.com/p/45ad65690e33# 这篇文章中讲的是spring中使用spring data jpa,使用了xml配置文件.我现在使用的是spring boot ,没有了xml文件配置就方便多了.我同样尝试了两种方式,也都是简单的查询,需要更复杂的查询,还需要我研究研究.往下看,需要先配置springboot的开发环境,需要大致了解springboot,这里可以看下面两篇文章: springboot 项目新建 springboot

Springboot中使用缓存

在开发中,如果相同的查询条件去频繁查询数据库, 是不是会给数据库带来很大的压力呢?因此,我们需要对查询出来的数据进行缓存,这样客户端只需要从数据库查询一次数据,然后会放入缓存中,以后再次查询时可以从缓存中读取.Spring3开始提供了强大的基于注解的缓存支持,可以通过注解配置方式低侵入的给原有Spring应用增加缓存功能,提高数据访问性能. 具体在Springboot中使用缓存如下: 1.在pom.xml中引入cache依赖,添加如下内容: <dependency> <groupId&g

【转】Power System 中基于 VIOS 的虚拟以太网实现

基于 VIOS 的虚拟以太网适配器的工作原理和配置实现 本文对 Power 系统中基于 VIOS 的虚拟以太网适配器(Virtual Ethernet Adapter)的工作原理.基本配置选项和配置步骤进行了讲解,并介绍了两种常用的 High Availability 的配置场景.Power System 的系统工程师可以通过本文了解虚拟以太网适配器的配置方法,软件工程师则可以学习到虚拟以太网适配器的工作原理. PowerVM 中相关概念简介 Power System 通过 PowerVM 软件