契约测试 之 常用注解解释、DSL提供的API使用、不同实体类的consumer测试写法、Provider测试写法,提供完整代码

Contract testing

1. Pact与其他工具的对比

Pact与其他工具的对比

主要有:

  • Spring Cloud Contract
  • Accurest
  • Nock
  • VCR
  • Webmock
  • Pacto

2. 支持的语言

  • JS
  • Java
  • Net
  • Go
  • Python
  • Swift
  • Scala
  • PHP
  • Ruby
  • C++

3. 依赖

3.1 Consumer

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-consumer-junit5</artifactId>
    <version>4.0.4</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-consumer-java8</artifactId>
    <version>4.0.4</version>
</dependency>

3.2 Provider

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-provider-junit5</artifactId>
    <version>${pact.version}</version>
    <scope>test</scope>
</dependency>

4. annotation

4.1 Consumer

4.1.1 @ExtendWith(PactConsumerTestExt.class)
  • JUnit5
  • 加在consumer unit test的文件上
  • 用于替代JUit 4的PactRunner
@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) 一起使用

官方例子

@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后做一些额外的操作

@State("SomeProviderState", action = StateChangeAction.TEARDOWN)
public void someProviderStateCleanup() {
  // Do what you need to to teardown the state
}

5. DSL - Consumer 代码

Pact consumer - 参考资料

5.1 不同类型的校验方式

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为

{
   "flag": true,
   "phoneNumber": 100,
   "address": "string",
   "code": "PKV92",
   "company": "Tencent",
   "id": 1,
   "delete": true
}
@ExtendWith({PactConsumerTestExt.class})
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Tag("ContractTest")
public class ConsumerTest {

    RestTemplate restTemplate;

    @BeforeEach
    public void initialRestTemplate() {
        restTemplate = new RestTemplate();
    }

    private Map<String, String> jsonHeader() {
        Map<String, String> 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/目录下会生成对应的契约文件

{
  "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为

{
    "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中的校验逻辑进行更改

/**
 * 要求:请求返回的对象中,属性名是: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对应的代码

5.3 返回的是List

此例子对应的Object Json为

[
    {
        "orderId":100,
        "ifPay":true,
        "orderName":"string"
    }
]
/**
 * 要求:请求返回的数组中,包含的每一个对象要符合以下条件,否则校验失败
 * 有:arrayEachLike、arrayMinLike、arrayMaxLike三种方式
 */ 
PactDslJsonArray.arrayEachLike() // arrayMinLike, arrayMaxLike
                        .numberType("orderId")
                        .stringType("orderName")
                        .booleanType("ifPay")

5.4 List包含List

此例子对应的Object Json为

[
    {
        "goodList":[
            {
                "goodName":"string",
                "goodId":100,
                "goodPrice":100
            }
        ],
        "orderId":100,
        "storeName":"string"
    }
]
// 关键在于.array & .object
PactDslJsonArray.arrayEachLike()
                        .numberType("orderId")
                        .stringType("storeName")
                        	.array("goodList")
                        		.object()
                        		.numberType("goodId")
                        		.stringType("goodName")
                        		.numberType("goodPrice")

5.5 Post请求校验

上面的demo都是Get请求的,Post请求如下:
大体类似,主要不同点在于,DSL中需要加入请求的参数。

// 请求的对象
@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;
}

测试

@ExtendWith({PactConsumerTestExt.class})
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Tag("ContractTest")
public class ConsumerTest5 {

    RestTemplate restTemplate;

    @BeforeEach
    public void initialRestTemplate() {
        restTemplate = new RestTemplate();
    }

    private Map<String, String> jsonHeader() {
        Map<String, String> 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对应的代码

5.6 请求路径的匹配方式

// before
.path("/findUserById/{id}")

// after
.matchPath("/findUserById/[0-9]+")

5.7 请求头的匹配方式

// before
.headers("Location", "/hello/1234")

// after
.matchHeaders("Location", "*/hello/[0-9]+", "/hello/1234")

6. Provider 代码

6.1 不同类型的校验方式

Provider完整的代码

@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数据不同

    @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请求校验

    @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. 参考资料

7.1 为什么要使用contract testing

8. 完整代码

ConsumerTest 对应 ProviderTest
ConsumerTest2 对应 ProviderTest2
以此类推
完整代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值