JSON断言

环境准备

这里以rest-assured官网的示例进行演示学习

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"lotto": {
"lottoId": 5,
"winning-numbers": [2, 45, 34, 23, 7, 5, 3],
"winners": [{
"winnerId": 23,
"numbers": [2, 45, 34, 23, 3, 5]
}, {
"winnerId": 54,
"numbers": [52, 3, 12, 11, 18, 22]
}]
}
}

可以使用WireMock进行响应规则的配置

JsonPath(Groovy’s GPath)

在 Groovy 的官网,虽然并未提及它在 json 中的使用,但实际上只要是树形的层级关系,无论是 json、xml 或者其他格式,就可以使用这种简单的语法帮我们去找到其中的值,rest-assured 也已经帮我们实现支持了 GPath 的断言方式。Groovy Gpath官网说明

根节点.子节点

  1. 我们可以使用根节点.(点)子节点的方式一层层的找下去,例如我们需要对lottoId等于 5 进行断言:

image-20210104170902159

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 可以使用根节点.(点)子节点的方式一层层的找下去,例如我们需要对lottoId等于 5 进行断言:
*/
@Test
void testGPathForNode01() {
given().
when().
log().all().
get("http://127.0.0.1:9090/api/json").
then().
log().all().
body("lotto.lottoId", equalTo(5));
}


  1. 如果我们想要断言winners数组下面的winnerId,检查23和54是否包含其中,可以如下lotto.winners.winnerId写法

image-20210104171050767

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 如果想要断言winners数组下面的winnerId,检查23和54是否包含其中,可以如下lotto.winners.winnerId写法
*/
@Test
void testGPathForNode02() {
given().
when().
log().all().
get("http://127.0.0.1:9090/api/json").
then().
log().all().
body("lotto.winners.winnerId", hasItems(54, 23));
}


索引取值

  1. 如果我们想要取某些相同字段中的某一个,可以使用类似索引的方式获取,例如想要断言 winners 数组下面的 winnerId 的第一个值是否为23,可以使用 lotto.winners.winnerId[0],写法如下:

image-20210104171227692

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 如果我们想要取某些相同字段中的某一个,可以使用类似索引的方式获取,例如想要断言 winners 数组下面的 winnerId 的第一个值是否为23,可以使用 lotto.winners.winnerId[0]
*/
@Test
void testGPathFoIndex01() {
given().
when().
log().all().get("http://127.0.0.1:9090/api/json").
then().
log().all().body("lotto.winners.winnerId[0]", equalTo(23));
}


  1. 如果我们想要取某些相同字段中的最后一个,可以使用 -1 作为索引,例如断言断言 winners 数组下面的 winnerId 的最后一个的值是否为 54

image-20210104171355494

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 如果我们想要取某些相同字段中的最后一个,可以使用 -1 作为索引,例如断言断言 winners 数组下面的 winnerId 的最后一个的值是否为 54
*/
@Test
void testGPathFoIndex02() {
given().
when().
log().all().
get("http://127.0.0.1:9090/api/json").
then().
log().all().
body("lotto.winners.winnerId[-1]", equalTo(54));
}

findAll

有时候我们需要获取符合某些条件的结果来进行断言,这里 findAll 可以帮助我们实现,我们可以在 findAll 方法中写筛选条件,例如我们想取 winnerId 的值在大于或等于 30 小于 60 之间的结果进行断言,具体写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 可以在 findAll 方法中写筛选条件,例如我们想取 winnerId 的值在大于或等于 30 小于 60 之间的结果进行断言
*/
@Test
void testGPathFoFindAll() {
given().
when().
log().all().
get("http://127.0.0.1:9090/api/json").
then().
log().all().
body("lotto.winners.findAll{ winners -> winners.winnerId >= 30 && winners.winnerId < 60}.winnerId[0]", equalTo(54));
}

find

find 的用法与 findAll 基本一致,只是 find 默认取匹配到的第一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
		/**
* find 的用法与 findAll 基本一致,只是 find 默认取匹配到的第一个
*/
@Test
void testGPathFoFind() {
given().
when().
log().all().
get("http://127.0.0.1:9090/api/json").
then().
log().all().
body("lotto.winners.find{ winners -> winners.winnerId >= 30 && winners.winnerId < 60}.winnerId", equalTo(54));
}
}

XML断言

环境准备

GPath 也支持 XML 格式的断言,这里再以 rest-assured 官方给的一个实例做演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="utf-8"?>

<shopping>
<category type="groceries">
<item>
<name>Chocolate</name>
<price>10</price>
</item>
<item>
<name>Coffee</name>
<price>20</price>
</item>
</category>
<category type="supplies">
<item>
<name>Paper</name>
<price>5</price>
</item>
<item quantity="4">
<name>Pens</name>
<price>15</price>
</item>
</category>
<category type="present">
<item when="Aug 10">
<name>Kathryn's Birthday</name>
<price>200</price>
</item>
</category>
</shopping>

XmlPath断言语法

若我们要对第二个 name 的值 Coffee 进行断言,写法如下:

image-20210104171922170

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 对第二个name的值Coffee进行断言
*/
@Test
void testXMLForIndex() {
given().
when().
get("http://127.0.0.1:9090/api/xml").
then().
log().all().
body("shopping.category[0].item[1].name", equalTo("Coffee"));
}

size()

可以利用 size() 方法来获取对应节点的数量,例如这里要断言 category 的数量:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 可以利用size()方法来获取对应节点的数量,例如这里要断言category的数量
*/
@Test
void testXMLForSize() {
given().
when().
get("http://127.0.0.1:9090/api/xml").
then().
log().all().
body("shopping.category.size()", equalTo(3));
}

it.@type、it.price

在 xml中 断言中,可以利用 it. 属性或节点的值来作为筛选条件;
例如这里要获取 type supplies category 下的第一个 item name,以及获取 price 为 10 的商品名 name

image-20210104172130774

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* it.@type、it.price
* 在 xml中 断言中,可以利用 it. 属性或节点的值来作为筛选条件;
* 例如这里要获取 type 为 supplies 的 category 下的第一个 item 的 name,以及获取 price 为 10 的商品名 name
*/
@Test
void testXMLForIt() {
given().
when().
get("/xml").
then().
log().all().
body("shopping.category.findAll{ it.@type == 'supplies' }.item[0].name", equalTo("Paper")).
body("shopping.category.item.findAll{ it.price == 10 }.name", equalTo("Chocolate"));
}

**.findAll

对于xml中有一个特别的语法,**.findAll,可以直接忽略前面的节点,直接对筛选条件进行匹配,依然获取price为10的商品名name,写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 对于xml中有一个特别的语法,**.findAll,可以直接忽略前面的节点,直接对筛选条件进行匹配,依然获取price为10的商品名name
*/
@Test
void testXMLForFindAll() {
given().
when().
get("/xml").
then().
log().all().
body("**.findAll{ it.price == 10 }.name", equalTo("Chocolate"));
}

Json-Schema断言

在实际工作中,对接口返回值进行断言校验,除了常用字段的断言检测以外,还要对其他字段的类型进行检测,原因在于:

  • 返回字段较多,无法保证每个字段都写断言
  • 防止客户端未做 null 值的校验判断,如果因为版本变更或网络等原因造成某个不能接收 null 值的返回字段为 null,就很有可能造成软件的崩溃
  • 某些数值是不能为负的
  • 小数点保留位数,对于股票的交易、医疗数据的分析,小数点的精确度都是有其实际价值的

对返回的字段一个个写断言显然是非常耗时的,这个时候就需要一个模板,可以定义好数据类型和匹配条件,除了关键参数外,其余可直接通过此模板来断言(JsonSchema)

Json-Schema简介

JsonSchema官方文档

基于JSON格式定义JSON数据结构的规范

  • 描述现有数据格式
  • 人类和机器可读
  • 完整的结构和数据验证

Json-Schema模板生成

  1. 首先要借助于Json schema tool的网站https://www.jsonschema.net/,将返回json字符串复制到页面左边,然后点击INFER SHCEMA,就会自动转换为schema json文件类型,会将每个地段的返回值类型都设置一个默认类型; 在pattern中也可以写正则进行匹配

    image-20210104172614172

  2. 点击“设置”按钮会出现各个类型返回值更详细的断言设置,这个就是schema最常用也是最实用的功能,也可以对每种类型的字段最更细化的区间值校验或者断言,例如长度,取值范围等,具体感兴趣的话可以从官网学习深入学习;平常对重要字段的校验我通常会选用其他断言,比如hamcrest断言

    image-20210104172601421

  3. 选择复制功能,可以将生成的schema模板保存下来与rest-assured结合使用

    image-20210104172631145

  4. 添加maven依赖,在rest-assured完成支持

  5. 使用matchesJsonSchemaInClasspath方法对响应结果进行schema断言

rest-assured结合使用

添加maven依赖,在rest-assured完成支持

1
2
3
4
5
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>${rest-assured.version}</version>
</dependency>

使用matchesJsonSchemaInClasspath方法对响应结果进行schema断言

Rest-Assured其他内建校验方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 通过Rest-Assured的内建校验方法实现验证
*/
@Test
void validateStatus() {
response.then().statusCode(200);
}

/**
* 断言响应header
*/
@Test
void validateHeader() {
response.then().header("Content-Type", containsString("json"));
}

/**
* 断言响应时间
*/
@Test
void validateResponseTime() {
response.then().time(lessThan(3000L));
}

/**
* 断言响应body
*/
@Test
void validateBody() {
response.then().body("owner.login", equalTo("jinglv"));
}

单元测试的断言

以上都是Rest-Assured的内建的断言机制,我们也可以结合单元测试框架的断言机制进行断言,以下是以Junit5为例

Junit5的断言

  • assertTrue、assertEquals…
  • assertAll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
void getParseResponse() {
// Rest-Assured jsonPath的使用
JsonPath jsonPath = new JsonPath(response.getBody().asString());
System.out.println("repo ID:" + jsonPath.get("id"));
// 设置根节点为owner
jsonPath.setRoot("owner");
System.out.println("owner ID:" + jsonPath.get("id"));

// Junit是的断言方式,存在多个断言,其中一个断言失败,则断言失败后续的代码都不会执行
// 使用Junit自带断言完成响应校验
assertTrue(response.getHeader("status").contains("OK"));
assertEquals(jsonPath.getInt("id"), 12013318);

// 把多个断言方法放在assertAll中,对多个断言的批量校验
assertAll("this is a group assert:",
() -> assertTrue(response.getHeader("status").contains("OK")),
() -> assertEquals(jsonPath.getInt("id"), 12013318));
}