Contract testing
1. 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 代码
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"
}
}
}
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")
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);
}
}
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
以此类推
完整代码
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/77865.html