探秘Spring Contract:如何保障您的API符合预期

发表于:2024-3-25 09:34

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:编码是个技术活    来源:今日头条

  1.什么是契约测试
  契约测试(Contract testing)是一种测试技术,它通过以隔离检查集成点上的每个应用的方式,确保应用发送或接收的消息符合调用双方共识,并允许随着时间的推移进行演化。
  契约测试是对单元测试的增强,针对服务接口provider测试,覆盖了一部分本来需要集成测试才能测试到的场景。
  2.为什么要做契约测试
  契约测试主要解决在存在沟通边界情况下,测试替身(Test Double)与生产代码表现可能不一致的问题。在契约测试中,契约由代码生成,保持与现实同步,而且应用可以独立于其它应用而仅基于契约进行快速测试。
  由于集成测试容易受到网络缓慢或不可靠,以及服务不可靠等因素的影响而运行缓慢或失败,所以通常会引入测试替身来代替真实外部服务,以快速完成覆盖度更广的测试,让测试真正起到作用。
  3.契约测试的定位
  金字塔模型是构建健康、快速、可维护测试集的成熟理论。
  4.契约测试的价值
  众所周知,越是在项目生命周期的后期发现Bug,其修复的成本就越高。
  不同于端到端(E2E)测试,契约测试可以在开发人员推送代码之前运行,在开发阶段提早发现问题。
  契约测试还有很多端到端测试不具备的好处:
  ·不需要调用其它组件,运行得很快。
  · 编写测试不需要了解系统全貌,更容易维护。
  · 问题只存在于被测试组件中,更容易调试和修复。
  · 极易反复运行。
  · 每个组件独立测试,不会引发流水线构建时间大幅增长。
  引入契约测试,还会带来如下福利:
  · 在提供者API就绪之前就可以开发消费者应用。
  · 为提供者供应准确的需求
  · 会收获一组文档化良好的用例,它们确切地显示了如何使用提供者。
  · 提供者对API变更更有信息,可以准确知道使用者感兴趣的字段,方便地移除未使用的字段,以及添加新的字段。
  对提供者API进行修改,可以立即看到会影响哪些使用者。
  没有两个团队是完全一样的,契约测试也不是万能的,关键要看契约测试可以为团队和项目带来什么。
  5.契约测试适合的场景
  契约测试可以用于任何需要通信的两个服务,比如Web前端与后端API服务。
  在微服务架构体系中,因为存在更多团队独立、服务间调用及服务单独演进的情形,契约测试有了更好更大的用武之地。良好的契约测试,使得开发人员很容易避免版本地狱,是微服务开发和部署的利器。
  6.概念术语
  契约测试主要涉及如下概念术语:
  · 消费者(Consumer):对于调用,发起请求的一方。对于MQ,为接收消息的一方。
  · 提供者(Provider):对于调用,响应请求的一方。对于MQ,为生成消息的一方。
  · 契约(Contract):消费者和提供者之间的共识,是一系列交互的集合。对于HTTP调用,包括描述消费者向提供者发送什么的预期请求,以及描述消费者希望提供者返回的最小期望响应。对于消息交互,则描述消费者希望得到的最小期望消息。
  7.契约测试模式
  契约测试分为消费者驱动(consumer-driven)和提供者驱动(Provider-driven)两种模式。
  消费者驱动更具哲学意义,将API的消费者置于设计过程的核心,来倡导更好的内部微服务设计。该模式的优点在于,只有消费者正在使用的部分会得到测试,而提供者可以自由地更改消费者不使用的任何其它部分,而不必破坏任何现有测试。
  提供者驱动思路较为常规,更适合开放数据或系统的场景。
  无论采用哪种风格,关键在于获得契约测试的好处,实现引入契约测试的目的。
  (1)消费者驱动
  消费者驱动的契约测试运行步骤如下:
  ·步骤1:消费者端
  · 编写并运行单元测试(包括对接口的请求参数和预期响应)
  · MockService代替实际服务提供者(自动)
  · 生成契约文件(自动)
  · 步骤2:提供者端
  · 启动服务提供者
  · 重放契约文件中的请求,验证真实响应是否满足预期(自动)
  (2)提供者驱动
  提供者驱动模式由提供者定义契约并驱动整个过程。
  8.契约测试工具
  流行的契约测试工具为:
  · Pact:是一个命令行工具,反馈时间更短,有助于消费者和生产者之间更好地沟通,支持与Maven/Gradle等集成。
  · Spring Cloud Contract:主要用于JVM环境,也容易扩展到非JVM环境,主要适用于生产者驱动的契约测试。
  9.利用Pact进行消费者驱动的测试价值
  利用Pact进行契约测试的整个流程示意如下,使用了 pact 之后,依然是每个服务独立的进行单元测试,但是可以模拟出真实集成场景。
  · 将一个笨重的集成测试化为两个容易编写、容易运行的单元测试/接口测试
  · 解耦消费者与提供者,甚至可以在没有提供者实现的情况下开展消费者端测试
  · 通过测试保证契约和实现的一致性,测试通过之时就是代码实现完成之时
  · 测试前移:在开发阶段就应该运行,并作为CI的一部分,便于尽早发现问题
  10.Pact示例
  pact契约测试分为两步:
  · 编写test用例,生成契约文件(不需要启动服务)。
  · 利用pact-verifier命令和契约文件,验证接口提供者是否正确 (需要启动提供者服务)
  以下为nlp-pact-parent示例:
  (1)父项目pom包相关
  <dependencies>
  <!-- contract testing -->
  <dependency>
      <groupId>au.com.dius</groupId>
      <artifactId>pact-jvm-consumer-junit5</artifactId>
      <version>4.0.10</version>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>au.com.dius</groupId>
      <artifactId>pact-jvm-provider-junit5</artifactId>
      <version>4.0.10</version>
      <scope>test</scope>
  </dependency>
  (2)nlp-pact-consumer消费端项目
  编写ConsumerTest生成契约:
  @ExtendWith(PactConsumerTestExt.class)
  @SpringBootTest(classes = PactConsumerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
  @PactTestFor(providerName = "nlp-pact-provider", port = "8202")
  public class ConsumerTest {
     private TestRestTemplate restTemplate = new TestRestTemplate();
     @Test
     @PactTestFor(pactMethod = "greetingPact")
      void greeting_shouldReturnMessage() {
        // Arrange
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
        // Act
        ResponseEntity<Map> response = restTemplate.getForEntity("http://localhost:8202/greeting?name=John", Map.class);
        // Assert
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals(Collections.singletonMap("message", "Hello, John!"), response.getBody());
     }
     // Pact 定义
     @Pact(consumer = "nlp-pact-consumer", provider = "nlp-pact-provider")
     public RequestResponsePact greetingPact(PactDslWithProvider builder) {
        return builder
              .given("a request for greeting with name 'John'")
              .uponReceiving("a request to greet John")
              .path("/greeting")
              .method("GET")
              .query("name=John")
              .willRespondWith()
              .status(200)
              .headers(Collections.singletonMap("Content-Type", "application/json"))
              .body("{\"message\": \"Hello, John!\"}")
              .toPact();
     }
  }
  (3)执行ConsumerTest测试用例,生成如下契约文件:
  # nlp-pact-consumer-nlp-pact-provider.json
  {
    "provider": {
      "name": "nlp-pact-provider"
    },
    "consumer": {
      "name": "nlp-pact-consumer"
    },
    "interactions": [
      {
        "description": "a request to greet John",
        "request": {
          "method": "GET",
          "path": "/greeting",
          "query": {
            "name": [
              "John"
            ]
          }
        },
        "response": {
          "status": 200,
          "headers": {
            "Content-Type": "application/json"
          },
          "body": {
            "message": "Hello, John!"
          }
        },
        "providerStates": [
          {
            "name": "a request for greeting with name 'John'"
          }
        ]
      }
    ],
    "metadata": {
      "pactSpecification": {
        "version": "3.0.0"
      },
      "pact-jvm": {
        "version": "4.0.10"
      }
    }
  }
  (4)nlp-pact-provider提供端验证契约
  build配置如下:
  <plugin>
      <groupId>au.com.dius</groupId>
      <artifactId>pact-jvm-provider-maven</artifactId>
      <version>4.0.0</version>
      <configuration>
          <serviceProviders>
              <!-- You can define as many as you need, but each must have a unique name -->
              <serviceProvider>
                  <name>nlp-pact-provider</name>
                  <!-- All the provider properties are optional, and have sensible defaults (shown below) -->
                  <protocol>http</protocol>
                  <host>localhost</host>
                  <port>8200</port>
                  <path>/</path>
                  <pactFileDirectory>resources/pacts</pactFileDirectory>
              </serviceProvider>
          </serviceProviders>
          <pactBrokerUrl/>
      </configuration>
  </plugin>
  (5)运行命令:mvn pact:verify,验证契约
  Found 1 pact files
  Verifying a pact between nlp-pact-consumer and nlp-pact-provider
    [Using File D:\IdeaProjects\nlp-other-project-dev\nlp-pact-parent\nlp-pact-provider\target\pacts\nlp-pact-consumer-nlp-pact-provider.json]
    Given a request for greeting with name 'John'
           WARNING: State Change ignored as there is no stateChange URL
    a request to greet John
      returns a response which
        has status code 200 (OK)
        has a matching body (OK)
  [WARNING] Skipping publishing of verification results as it has been disabled (pact.verifier.publishResults is not 'true')
  [INFO] ------------------------------------------------------------------------
  [INFO] BUILD SUCCESS
  [INFO] ------------------------------------------------------------------------
  [INFO] Total time:  1.589 s
  [INFO] Finished at: 2023-03-08T15:02:33+08:00
  [INFO] --------
  11.Pact Broker
  Pact Broker是一个用于共享消费者驱动的合同和验证结果的应用程序。
  pact主页面:
  查看服务间关系:
  与CICD集成:
  12.总结
  我在不少项目中都尝试过实施契约测试,但是真正实施成功的并不多,主要原因还是规模和痛点不够大,从而导致团队觉得没有必要做,或者觉得做了收益比投入少。而成功的一般的都是团队人员足够痛,或者经历过大型多团队项目中服务改变等各种痛点,从而导致他们解决自己的痛点而主动实施契约测试,但是前提是他们都知道契约测试。所以要成功实施契约都是有两个主要的前提条件:1,团队对于相关问题足够痛,2,团队懂契约测试。在这种情况下,团队才可能愿意主动实施契约测试,才能成功的实施契约测试。所以首先是要让开发团队懂契约测试,比如契约测试能解决什么问题,实施流程,相关测试框架等,然后等待团队无法忍受相关痛点后,成功的实施契约测试就可以水到渠成了。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号