1. Overview
1.概述
In this quick article, we’ll be looking at the concept of Consumer-Driven Contracts.
在这篇简短的文章中,我们将看看消费者驱动合同的概念。
We’ll be testing integration with an external REST service through a contract that we define using the Pact library. That contract can be defined by the client, then picked up by the provider and used for development of its services.
我们将通过使用Pact库定义的合同来测试与外部 REST 服务的集成。该合约可由客户定义,然后由提供商拾取并用于其服务的开发。
We’ll also create tests based on the contract for both the client and provider applications.
我们还将为客户和供应商的应用程序创建基于合同的测试。
2. What Is Pact?
2.什么是契约?
Using Pact, we can define consumer expectations for a given provider (that can be an HTTP REST service) in the form of a contract (hence the name of the library).
使用Pact,我们可以以合同的形式定义消费者对特定提供者(可以是HTTP REST服务)的期望(因此该库的名称)。
We’re going to set up this contract using the DSL provided by Pact. Once defined, we can test interactions between consumers and the provider using the mock service that is created based on the defined contract. Also, we’ll test the service against the contract by using a mock client.
我们将使用Pact提供的DSL来设置这个合约。一旦定义好,我们就可以使用基于定义好的契约创建的模拟服务来测试消费者和提供者之间的交互。此外,我们还将通过使用模拟客户端来测试服务与合同的关系。
3. Maven Dependency
3.Maven的依赖性
To get started we’ll need to add Maven dependency to pact-jvm-consumer-junit5_2.12 library:
为了开始工作,我们需要向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. Defining a Contract
4.合同的定义
When we want to create a test using Pact, first we need to annotate our test class with the provider that will be used:
当我们想使用Pact创建一个测试时,首先我们需要用将要使用的提供者来注释我们的测试类。
@PactTestFor(providerName = "test_provider", hostInterface="localhost")
public class PactConsumerDrivenContractUnitTest
We’re passing the provider name and host on which the server mock (which is created from the contract) will be started.
我们要传递提供者的名字和主机,服务器模拟(从合同中创建)将在其上启动。
Let’s say that service has defined the contract for two HTTP methods that it can handle.
假设该服务已经定义了它可以处理的两个HTTP方法的契约。
The first method is a GET request that returns JSON with two fields. When the request succeeds, it returns a 200 HTTP response code and the Content-Type header for JSON.
第一个方法是一个GET请求,返回有两个字段的JSON。当请求成功时,它返回一个200 HTTP响应代码和JSON的Content-Type 头。
Let’s define such a contract using Pact.
让我们用Pact来定义这样一个合约。
We need to use the @Pact annotation and pass the consumer name for which the contract is defined. Inside of the annotated method, we can define our GET contract:
我们需要使用@Pact 注解,并传递定义了合同的消费者名称。在注解的方法中,我们可以定义我们的GET合同。
@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\"}")
(...)
}
Using the Pact DSL we define that for a given GET request we want to return a 200 response with specific headers and body.
使用Pact DSL,我们定义对于一个给定的GET请求,我们希望返回一个带有特定头信息和正文的200响应。
The second part of our contract is the POST method. When the client sends a POST request to the path /pact with a proper JSON body it returns a 201 HTTP response code.
我们合同的第二部分是POST方法。当客户端向路径/pact 发送一个POST请求,并带有适当的JSON主体时,它会返回一个201 HTTP响应代码。
Let’s define such contract with Pact:
让我们用Pact:来定义这种合同。
(...)
.given("test POST")
.uponReceiving("POST REQUEST")
.method("POST")
.headers(headers)
.body("{\"name\": \"Michael\"}")
.path("/pact")
.willRespondWith()
.status(201)
.toPact();
Note that we need to call the toPact() method at the end of the contract to return an instance of RequestResponsePact.
请注意,我们需要在合同的末尾调用toPact() 方法来返回RequestResponsePact的实例。
4.1. Resulting Pact Artifact
4.1.产生的契约神器
By default, Pact files will be generated in the target/pacts folder. To customize this path, we can configure the maven-surefire-plugin:
默认情况下,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>
The Maven build will generate a file called test_consumer-test_provider.json in the target/mypacts folder which contains the structure of the requests and responses:
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. Testing the Client and Provider Using the Contract
5.使用合同测试客户和提供者
Now that we have our contract, we can use to create tests against it for both the client and the provider.
现在我们有了我们的合同,我们可以用它来为客户和供应商创建测试。
Each of these tests will use a mock of its counterpart which is based on the contract, meaning:
这些测试中的每一个都将使用其对应的模拟,这是以合同为基础的,意味着。
- the client will use a mock provider
- the provider will use a mock client
Effectively, the tests are done against the contract.
实际上,测试是针对合同进行的。。
5.1. Testing the Client
5.1.测试客户端
Once we defined the contract we can test interactions with the service that will be created based on that contract. We can create normal JUnit test but we need to remember to put the @PactTestFor annotation at the beginning of the test.
一旦我们定义了合同,我们就可以测试与将基于该合同创建的服务的交互。我们可以创建普通的JUnit测试,但是我们需要记住在测试的开始部分加上@PactTestFor 注解。
Let’s write a test for the GET request:
让我们为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");
}
The @PactTestFor annotation takes care of starting the HTTP service, and can be put either on the test class, or on the test method. In the test, we only need to send the GET request and assert that our response complies with the contract.
@PactTestForannotation负责启动HTTP服务,可以放在测试类上,也可以放在测试方法上。在测试中,我们只需要发送GET请求,并断言我们的响应符合合同。
Let’s add the test for the POST method call as well:
让我们也为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);
As we can see, the response code for the POST request is equal to 201 – exactly as it was defined in the Pact contract.
我们可以看到,POST请求的响应代码等于201–与Pact合同中定义的完全一样。
As we were using the @PactTestFor() annotation, the Pact library is starting the web server based on the previously defined contract before our test case.
由于我们使用了@PactTestFor() annotation,Pact library在我们的测试用例之前基于先前定义的合同启动了Web服务器。
5.2. Testing the Provider
5.2.测试提供者
The second step of our contract verification is creating a test for the provider using a mock client based on the contract.
我们合同验证的第二步是使用基于合同的模拟客户为提供者创建一个测试。
Our provider implementation will be driven by this contract in TDD fashion.
我们的供应商的实施将由这个合同以TDD的方式驱动。
For our example, we’ll use a Spring Boot REST API.
在我们的例子中,我们将使用一个Spring Boot REST API。
First, to create our JUnit test, we’ll need to add the pact-jvm-provider-junit5_2.12 dependency:
首先,为了创建我们的 JUnit 测试,我们需要添加 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>
This allows us to create a JUnit test specifying the provider name and the location of the Pact artifact:
这允许我们创建一个JUnit测试,指定提供者名称和Pact工件的位置:。
@Provider("test_provider")
@PactFolder("pacts")
public class PactProviderLiveTest {
//...
}
For this configuration to work, we have to place the test_consumer-test_provider.json file in the pacts folder of our REST service project.
为了使这个配置发挥作用,我们必须把test_consumer-test_provider.json文件放在我们的REST服务项目的pacts文件夹中。
Next, for writing Pact verification tests with JUnit 5, we need to use PactVerificationInvocationContextProvider with the @TestTemplate annotation. We’ll need to pass it the PactVerificationContext parameter, which we’ll use to set the target Spring Boot application details:
接下来,为了用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"));
}
Finally, we’ll specify the states in the contract that we want to test:
最后,我们将在合同中指定我们要测试的状态。
@State("test GET")
public void toGetState() { }
@State("test POST")
public void toPostState() { }
Running this JUnit class will execute two tests for the two GET and POST requests. Let’s take a look at the log:
运行这个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)
Note that we haven’t included the code for creating a REST service here. The full service and test can be found in the GitHub project.
请注意,我们这里没有包括创建REST服务的代码。完整的服务和测试可以在GitHub项目中找到。
6. Conclusion
6.结论
In this quick tutorial, we had a look at Consumer Driven Contracts.
在这个快速教程中,我们看了一下消费者驱动的合同。
We created a contract using the Pact library. Once we defined the contract, we were able to test the client and service against the contract and assert that they comply with the specification.
我们使用Pact库创建了一个合同。一旦我们定义了合同,我们就能够根据合同测试客户端和服务,并断言它们符合规范。
The implementation of all these examples and code snippets can be found in the GitHub project – this is a Maven project, so it should be easy to import and run as it is.
所有这些例子和代码片段的实现都可以在GitHub项目中找到–这是一个Maven项目,所以应该很容易导入并按原样运行。