# 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)