pact java 测试_Contract test

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

au.com.dius

pact-jvm-consumer-junit5

4.0.4

test

au.com.dius

pact-jvm-consumer-java8

4.0.4

3.2 Provider

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

@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 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/目录下会生成对应的契约文件

{

"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 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);

}

}

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

以此类推

完整代码

你可能感兴趣的:(pact,java,测试)