# Contract test
**Repository Path**: Protector_hui/contract-test
## Basic Information
- **Project Name**: Contract test
- **Description**: 契约测试
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2020-08-31
- **Last Updated**: 2021-04-20
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Contract testing
## 1. Pact与其他工具的对比
[Pact与其他工具的对比](https://docs.pact.io/getting_started/comparisons)
**主要有:**
- Spring Cloud Contract
- Accurest
- Nock
- VCR
- Webmock
- Pacto
## 2. 支持的语言
- JS
- Java
- Net
- Go
- Python
- Swift
- Scala
- PHP
- Ruby
- C++
## 3. 依赖
### 3.1 Consumer
```xml
au.com.dius
pact-jvm-consumer-junit5
4.0.4
test
au.com.dius
pact-jvm-consumer-java8
4.0.4
```
### 3.2 Provider
```xml
au.com.dius
pact-jvm-provider-junit5
${pact.version}
test
```
## 4. annotation
### 4.1 Consumer
#### 4.1.1 @ExtendWith(PactConsumerTestExt.class)
- JUnit5
- 加在**consumer unit test**的文件上
- 用于替代**JUit 4的PactRunner**
```java
@ExtendWith(PactConsumerTestExt.class)
class ExampleJavaConsumerPactTest {
```
### 4.1.2 @Pact(provider="ArticlesProvider", consumer="test_consumer")
对于每个测试,需要定义一个用 **@Pact** 注释的方法。
### 4.1.3 @PactTestFor(providerName = "ArticlesProvider")
- 通过 **@PactTestFor** 链接 mock server 与 test 交互。
- 此方法可以加到测试类上,也可以加到测试方法上。
- hostname不填的话,默认是:localhost
- port不填的话,默认是:随机端口号
### 4.2 Provider
#### 4.2.1 @TestTemplate
这个注解会在consumer生成的契约文件中,找到所有的交互,并且为provider生成一个个对应的测试。
需配合 **@ExtendWith(PactVerificationSpringProvider.class)** 一起使用
**官方例子**
```java
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@Provider("Animal Profile Service")
@PactBroker
public class ContractVerificationTest {
@TestTemplate
@ExtendWith(PactVerificationSpringProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
}
```
#### 4.2.2 @Provider("Animal Profile Service")
设置用于测试的Provider的名称,与Consumer test中 **@Pact(provider = "Animal Profile Service")** 对应
#### 4.2.3 @PactFolder("pacts")
指定consumer test生成契约的位置,通常是:**../target/pacts/**
#### 4.2.4 @State("query user")
对应consumer test中DSL的.given的值。
此方法会在调用我们程序API之前先被调用,这里面可以做一些mock数据的操作等。
#### 4.2.5 @State("SomeProviderState", action = StateChangeAction.TEARDOWN)
在前面的基础上,加多了:**action = tateChangeAction.TEARDOWN**,次方法会在调用完我们程序API后做一些额外的操作
```java
@State("SomeProviderState", action = StateChangeAction.TEARDOWN)
public void someProviderStateCleanup() {
// Do what you need to to teardown the state
}
```
## 5. DSL - Consumer 代码
[Pact consumer - 参考资料](https://docs.pact.io/implementation_guides/jvm/consumer)
### 5.1 不同类型的校验方式
```java
LambdaDsl.newJsonBody(o -> o
// value值层面上做比较
.numberValue("id", 1)
.stringValue("company", "Tencent")
.booleanValue("flag", true)
// 数据类型上做限制,不在乎对应的value值
.numberType("phoneNumber")
.stringType("address")
.booleanType("delete")
// 用正则表达式匹配value值
.stringMatcher("code", "[A-Z]{3}\\d{2}")
).build()
```
**consumer完整的例子**
**此例子对应的Object Json为**
```json
{
"flag": true,
"phoneNumber": 100,
"address": "string",
"code": "PKV92",
"company": "Tencent",
"id": 1,
"delete": true
}
```
```java
@ExtendWith({PactConsumerTestExt.class})
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Tag("ContractTest")
public class ConsumerTest {
RestTemplate restTemplate;
@BeforeEach
public void initialRestTemplate() {
restTemplate = new RestTemplate();
}
private Map jsonHeader() {
Map map = new HashMap<>();
map.put("Content-Type", "application/json;charset=UTF-8");
return map;
}
@Pact(provider = "user", consumer = "queryUser")
public RequestResponsePact retrieveUserTask(PactDslWithProvider builder) {
return builder
.given("query user") // 对应provider的@State("query user")
.uponReceiving("for query user testing")
.path("/user/1") // 请求路径
.method("GET") // 请求方式
.willRespondWith() // 设定预期的请求返回值
.status(200)
.body(
LambdaDsl.newJsonBody(o -> o
.numberValue("id", 1)
.stringValue("company", "Tencent")
.booleanValue("flag", true)
.numberType("phoneNumber")
.stringType("address")
.booleanType("delete")
.stringMatcher("code", "[A-Z]{3}\\d{2}")
).build())
.headers(jsonHeader())
.toPact();
}
@Test
@PactTestFor(providerName = "user", port = "8585")
public void runTestRetrieveUserTask() {
restTemplate.getForObject("http://localhost:8585/user/{id}", UserInformationDto.class, 1);
}
}
```
**执行测试后,在target/pacts/目录下会生成对应的契约文件**
```json
{
"provider": {
"name": "user"
},
"consumer": {
"name": "queryUser"
},
"interactions": [
{
"description": "for query user testing",
"request": {
"method": "GET",
"path": "/user/1"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json;charset\u003dUTF-8"
},
"body": {
"flag": true,
"phoneNumber": 100,
"address": "string",
"code": "PKV92",
"company": "Tencent",
"id": 1,
"delete": true
},
"matchingRules": {
"body": {
"$.phoneNumber": {
"matchers": [
{
"match": "number"
}
],
"combine": "AND"
},
"$.address": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.delete": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.code": {
"matchers": [
{
"match": "regex",
"regex": "[A-Z]{3}\\d{2}"
}
],
"combine": "AND"
}
},
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json(;\\s?charset\u003d[\\w\\-]+)?"
}
],
"combine": "AND"
}
}
},
"generators": {
"body": {
"$.phoneNumber": {
"type": "RandomInt",
"min": 0,
"max": 2147483647
},
"$.address": {
"type": "RandomString",
"size": 20
},
"$.code": {
"type": "Regex",
"regex": "[A-Z]{3}\\d{2}"
}
}
}
},
"providerStates": [
{
"name": "query user"
}
]
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "4.0.4"
}
}
}
```
[Provider对应的代码](#不同类型的校验方式)
### 5.2 某个对象属性是List
**此例子对应的Object Json为**
```json
{
"userInformationDtoList":[
{
"phoneNumber":100,
"address":"string",
"code":"string",
"flag":true,
"company":"string",
"id":100,
"delete":true
},
{
"phoneNumber":100,
"address":"string",
"code":"string",
"flag":true,
"company":"string",
"id":100,
"delete":true
}
]
}
```
其他的和上面例子一样,就是.body中的校验逻辑进行更改
```java
/**
* 要求:请求返回的对象中,属性名是:userInformationDtoList的List,至少有两个以上的对象要符合以下条件,否则校验失败
* 有:minArrayLike、maxArrayLike、eachLike 三种方式
*/
new PactDslJsonBody()
.minArrayLike("userInformationDtoList", 2) // maxArrayLike, eachLike
.numberType("id")
.numberType("phoneNumber")
.stringType("company")
.stringType("address")
.stringType("code")
.booleanType("flag")
.booleanType("delete")
```
[Provider对应的代码](#某个对象属性是List)
### 5.3 返回的是List
**此例子对应的Object Json为**
```json
[
{
"orderId":100,
"ifPay":true,
"orderName":"string"
}
]
```
```java
/**
* 要求:请求返回的数组中,包含的每一个对象要符合以下条件,否则校验失败
* 有:arrayEachLike、arrayMinLike、arrayMaxLike三种方式
*/
PactDslJsonArray.arrayEachLike() // arrayMinLike, arrayMaxLike
.numberType("orderId")
.stringType("orderName")
.booleanType("ifPay")
```
### 5.4 List包含List
**此例子对应的Object Json为**
```json
[
{
"goodList":[
{
"goodName":"string",
"goodId":100,
"goodPrice":100
}
],
"orderId":100,
"storeName":"string"
}
]
```
```java
// 关键在于.array & .object
PactDslJsonArray.arrayEachLike()
.numberType("orderId")
.stringType("storeName")
.array("goodList")
.object()
.numberType("goodId")
.stringType("goodName")
.numberType("goodPrice")
```
### 5.5 Post请求校验
上面的demo都是Get请求的,Post请求如下:
大体类似,主要不同点在于,DSL中需要加入请求的参数。
```java
// 请求的对象
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RequestDto {
private int id;
private String name;
}
// 返回的对象
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseDto {
private int id;
private String name;
private int phoneNumber;
}
```
**测试**
```java
@ExtendWith({PactConsumerTestExt.class})
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Tag("ContractTest")
public class ConsumerTest5 {
RestTemplate restTemplate;
@BeforeEach
public void initialRestTemplate() {
restTemplate = new RestTemplate();
}
private Map jsonHeader() {
Map map = new HashMap<>();
map.put("Content-Type", "application/json;charset=UTF-8");
return map;
}
@Pact(provider = "userInfo", consumer = "queryUserInfo")
public RequestResponsePact retrieveUserInfo(PactDslWithProvider builder) {
RequestDto requestDto = RequestDto
.builder()
.id(1)
.name("Dwayne")
.build();
return builder
.given("retrieveUserInfo 1")
.uponReceiving("UserInfo of 1 is returned")
.path("/findUserInfoById")
.method("POST")
.body(JSONObject.toJSONString(requestDto)) // 这里比GET请求多了一个存放请求参数的body
.willRespondWith()
.status(200)
.body(LambdaDsl
.newJsonBody(o -> o
.numberValue("id", 1)
.stringValue("name", "Dwayne")
.numberType("phoneNumber")
).build())
.headers(jsonHeader())
.toPact();
}
@Test
@PactTestFor(providerName = "userInfo", port = "8585")
public void runTestRetrieveUserInfo() {
RequestDto requestDto = RequestDto
.builder()
.id(1)
.name("Dwayne")
.build();
// restTemplate的请求方式也需要改变
restTemplate.postForObject("http://localhost:8585/findUserInfoById", requestDto, ResponseDto.class);
}
}
```
[Provider对应的代码](#Post请求校验)
### 5.6 请求路径的匹配方式
```java
// before
.path("/findUserById/{id}")
// after
.matchPath("/findUserById/[0-9]+")
```
### 5.7 请求头的匹配方式
```java
// before
.headers("Location", "/hello/1234")
// after
.matchHeaders("Location", "*/hello/[0-9]+", "/hello/1234")
```
## 6. Provider 代码
### 6.1 不同类型的校验方式
**Provider完整的代码**
```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Provider("user")
@Tag("ContractTest")
@PactFolder("D:\\eclipse-workspace\\Pact Practice\\Pact Demo\\target\\pacts")
public class ProviderTest {
@LocalServerPort
int localServerPort;
@MockBean
UserTaskService userTaskService;
@BeforeEach
void setupTestTarget(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", localServerPort, "/"));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context, HttpRequest request) {
context.verifyInteraction();
}
@State("query user")
public void retrieveUserTaskVerify() {
UserInformationDto expectUserTaskDto = UserInformationDto.builder()
.id(1)
.company("TEST")
.flag(true)
.phoneNumber(123456)
.address("address test")
.delete(false)
.code("ABC01")
.build();
doReturn(expectUserTaskDto).when(userTaskService).findById(1);
}
}
```
### 6.2 某个对象属性是List
其他都一样,就是mock数据不同
```java
@State("retrieveUserTask 1")
public void retrieveUserTaskVerify() {
UserInformationDto expectUserTaskDto = UserInformationDto.builder()
.id(1)
.company("TEST")
.flag(true)
.phoneNumber(123456)
.address("address test")
.delete(false)
.code("ABC01")
.build();
UserListDto userListDto = UserListDto
.builder()
.userInformationDtoList(Arrays.asList(expectUserTaskDto, expectUserTaskDto))
.build();
doReturn(userListDto).when(userTaskService).findAll();
}
```
### 6.3 Post请求校验
```java
@State("retrieveUserInfo 1")
public void retrieveUserTaskVerify() {
RequestDto requestDto = RequestDto
.builder()
.id(1)
.name("Dwayne")
.build();
ResponseDto responseDto = ResponseDto
.builder()
.id(1)
.name("Dwayne")
.phoneNumber(123)
.build();
doReturn(responseDto).when(postService).findUserInfoById(requestDto);
}
```
## 7. 参考资料
- [Pact是如何工作的](https://pactflow.io/how-pact-works/?utm_source=ossdocs&utm_campaign=getting_started#slide-1)
- [Pact JVM](https://docs.pact.io/implementation_guides/jvm)
### 7.1 为什么要使用contract testing
- [为什么要使用contract testing](https://docs.pact.io/faq/convinceme)
- [集成测试是骗局](https://blog.thecodewhisperer.com/permalink/integrated-tests-are-a-scam)
## 8. 完整代码
ConsumerTest 对应 ProviderTest
ConsumerTest2 对应 ProviderTest2
以此类推
[完整代码](https://gitee.com/Protector_hui/contract-test)