1. 概述

本文将介绍消费者驱动契约(Consumer-Driven Contracts)的概念。我们将通过使用Pact库定义的契约,来测试与外部REST服务的集成。这个契约由客户端定义,然后被服务提供者获取并用于其服务开发。

我们还将基于该契约为客户端和服务提供者应用程序创建测试。

2. 什么是Pact?

使用Pact,我们可以以契约的形式定义消费者对给定提供者(可以是HTTP REST服务)的期望(这也是该库名称的由来)。

我们将使用Pact提供的DSL来设置这个契约。一旦定义完成,我们可以使用基于契约创建的模拟服务来测试消费者和提供者之间的交互。同时,我们还将使用模拟客户端来测试服务是否符合契约。

3. Maven依赖

首先需要添加pact-jvm-consumer-junit5_2.12库的Maven依赖:

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-consumer-junit5_2.12</artifactId>
    <version>3.6.3</version>
    <scope>test</scope>
</dependency>

4. 定义契约

使用Pact创建测试时,首先需要使用将要使用的提供者来注解测试类:

@PactTestFor(providerName = "test_provider", hostInterface="localhost")
public class PactConsumerDrivenContractUnitTest

我们传递提供者名称和服务器模拟(基于契约创建)将启动的主机地址。

假设服务为两个HTTP方法定义了契约:

  1. 第一个方法是GET请求,返回包含两个字段的JSON。请求成功时返回200 HTTP状态码和JSON的Content-Type头。
  2. 第二个方法是POST请求,当客户端向/pact路径发送带有正确JSON主体的POST请求时,返回201 HTTP状态码。

让我们使用Pact定义这些契约:

@Pact(consumer = "test_consumer")
public RequestResponsePact createPact(PactDslWithProvider builder) {
    Map<String, String> headers = new HashMap<>();
    headers.put("Content-Type", "application/json");

    return builder
      .given("test GET")
        .uponReceiving("GET REQUEST")
        .path("/pact")
        .method("GET")
      .willRespondWith()
        .status(200)
        .headers(headers)
        .body("{\"condition\": true, \"name\": \"tom\"}")
        (...)
}

使用Pact DSL,我们定义对于给定的GET请求,我们希望返回具有特定头部和主体的200响应。

契约的第二部分是POST方法:

(...)
.given("test POST")
.uponReceiving("POST REQUEST")
  .method("POST")
  .headers(headers)
  .body("{\"name\": \"Michael\"}")
  .path("/pact")
.willRespondWith()
  .status(201)
.toPact();

注意需要在契约末尾调用toPact()方法返回RequestResponsePact实例。

4.1. 生成的Pact文件

默认情况下,Pact文件会生成在target/pacts文件夹中。要自定义路径,可以配置maven-surefire-plugin

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <systemPropertyVariables>
            <pact.rootDir>target/mypacts</pact.rootDir>
        </systemPropertyVariables>
    </configuration>
    ...
</plugin>

Maven构建会在target/mypacts文件夹中生成名为test_consumer-test_provider.json的文件,包含请求和响应的结构:

{
    "provider": {
        "name": "test_provider"
    },
    "consumer": {
        "name": "test_consumer"
    },
    "interactions": [
        {
            "description": "GET REQUEST",
            "request": {
                "method": "GET",
                "path": "/"
            },
            "response": {
                "status": 200,
                "headers": {
                    "Content-Type": "application/json"
                },
                "body": {
                    "condition": true,
                    "name": "tom"
                }
            },
            "providerStates": [
                {
                    "name": "test GET"
                }
            ]
        },
        {
            "description": "POST REQUEST",
            ...
        }
    ],
    "metadata": {
        "pact-specification": {
            "version": "3.0.0"
        },
        "pact-jvm": {
            "version": "3.6.3"
        }
    }
}

5. 使用契约测试客户端和提供者

现在有了契约,我们可以为客户端和提供者创建基于契约的测试:

  • 客户端将使用模拟提供者
  • 提供者将使用模拟客户端

实际上,测试是针对契约进行的。

5.1. 测试客户端

定义契约后,我们可以测试与基于该契约创建的服务交互。可以创建普通的JUnit测试,但需要记住在测试开始处添加@PactTestFor注解。

GET请求的测试示例:

@Test
@PactTestFor
public void givenGet_whenSendRequest_shouldReturn200WithProperHeaderAndBody() {
 
    // when
    ResponseEntity<String> response = new RestTemplate()
      .getForEntity(mockProvider.getUrl() + "/pact", String.class);

    // then
    assertThat(response.getStatusCode().value()).isEqualTo(200);
    assertThat(response.getHeaders().get("Content-Type").contains("application/json")).isTrue();
    assertThat(response.getBody()).contains("condition", "true", "name", "tom");
}

@PactTestFor注解负责启动HTTP服务,可以放在测试类或测试方法上。 在测试中,我们只需发送GET请求并验证响应是否符合契约。

POST方法的测试:

HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
String jsonBody = "{\"name\": \"Michael\"}";

// when
ResponseEntity<String> postResponse = new RestTemplate()
  .exchange(
    mockProvider.getUrl() + "/create",
    HttpMethod.POST,
    new HttpEntity<>(jsonBody, httpHeaders), 
    String.class
);

//then
assertThat(postResponse.getStatusCode().value()).isEqualTo(201);

可以看到,POST请求的响应码为201——与Pact契约中定义的完全一致。

由于使用了@PactTestFor()注解,Pact库会在测试用例执行前根据之前定义的契约启动Web服务器。

5.2. 测试提供者

契约验证的第二步是为提供者创建测试,使用基于契约的模拟客户端。我们的提供者实现将以TDD方式由这个契约驱动。

示例中使用Spring Boot REST API。首先添加pact-jvm-provider-junit5_2.12依赖:

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-provider-junit5_2.12</artifactId>
    <version>3.6.3</version>
</dependency>

这允许我们创建指定提供者名称和Pact文件位置的JUnit测试:

@Provider("test_provider")
@PactFolder("pacts")
public class PactProviderLiveTest {
    //...
}

要使此配置生效,需要将test_consumer-test_provider.json文件放在REST服务项目的pacts文件夹中。

接下来,使用JUnit 5编写Pact验证测试时,需要将PactVerificationInvocationContextProvider@TestTemplate注解一起使用。需要传递PactVerificationContext参数,用于设置目标Spring Boot应用程序的详细信息:

private static ConfigurableWebApplicationContext application;

@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
    context.verifyInteraction();
}

@BeforeAll
public static void start() {
    application = (ConfigurableWebApplicationContext) SpringApplication.run(MainApplication.class);
}

@BeforeEach
void before(PactVerificationContext context) {
    context.setTarget(new HttpTestTarget("localhost", 8082, "/spring-rest"));
}

最后,指定要测试的契约状态:

@State("test GET")
public void toGetState() { }

@State("test POST")
public void toPostState() { }

运行这个JUnit类会执行两个测试(GET和POST请求)。查看日志:

Verifying a pact between test_consumer and test_provider
  Given test GET
  GET REQUEST
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json" (OK)
      has a matching body (OK)

Verifying a pact between test_consumer and test_provider
  Given test POST
  POST REQUEST
    returns a response which
      has status code 201 (OK)
      has a matching body (OK)

⚠️ 注意:这里没有包含创建REST服务的代码。完整的服务和测试可以在GitHub项目中找到。

6. 总结

本文介绍了消费者驱动契约测试。我们使用Pact库创建了契约,然后基于该契约测试了客户端和服务,并验证它们是否符合规范。

所有示例和代码片段的实现可以在GitHub项目中找到——这是一个Maven项目,应该可以直接导入运行。


原始标题:Consumer Driven Contracts with Pact