json响应体断言
json 响应体断言
简介
JSON响应断言是针对API测试中的响应数据进行验证的一种方法,通常用于确认API服务是否按照预期返回正确的数据。在自动化测试中,需要对接口的返回结果进行验证,以确保接口的功能和数据符合预期
- 服务器端响应客户端请求后返回的数据体。
- 接口响应体的格式通常为 JSON、XML、HTML 等。
JSON格式响应体:
{
"name": "John",
"age": 30,
"city": "New York"
}
使用场景
我们在接口返回响应的时候需要去看下我们的字段是否符合预期,但是当项目越写越多的时候就不太可能一个一个的去看返回的字段是否是符合预期,下面是简单响应和复杂响应的场景:
- 下面这段数据是
https://httpbin.ceshiren.com/get
接口的response
。
{
'args': {},
'headers': {'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate',
'Host': 'httpbin.ceshiren.com',
'Sw8':'1-MGRhYTczMDctMjQ1OC00ZjZkLWE4OWEtMGVlZDliNDdkMDZh-ZWJlNTI4NjItZTA3ZS00NjQ0LThlMWItODBjYmIwMDI5MWY4-1-QVBJU0lYLUJK-QVBJU0lYLUJK-L2dldA==-dXBzdHJlYW0gc2VydmljZQ==',
'User-Agent': 'python-requests/2.31.0',
'X-Forwarded-Host': 'httpbin.ceshiren.com'},
'origin': '36.112.118.254, 182.92.156.22',
'url': 'https://httpbin.ceshiren.com/get'
}
假设我们要查看 response
中的 url
是否是符合我们的预期:
import requests
import pprint
def res_json():
r = requests.get("https://httpbin.ceshiren.com/get")
pprint.pprint(r.json())
assert r.status_code == 200
assert r.json()["url"] == "https://httpbin.ceshiren.com/get"
res_json()
headers
中查看 host
预期是否一致:
def res_json():
r = requests.get("https://httpbin.ceshiren.com/get")
pprint.pprint(r.json())
assert r.status_code == 200
assert r.json()["headers"]["Host"] == "httpbin.ceshiren.com")
res_json()
多层嵌套的数据提取与断言: JSONPath
JSONPath 的使用场景
如何从一个非常复杂的 json 结构中,提取数据信息。
比如如下的响应信息:
// - 层级多。
// - 嵌套关系复杂。
{
"errcode": 0,
"errmsg": "ok",
"userid": "zhangsan",
"name": "张三",
"department": [1, 2],
"order": [1, 2],
"position": "后台工程师",
"mobile": "13800000000",
"gender": "1",
"email": "zhangsan@gzdev.com",
"biz_mail": "zhangsan@qyycs2.wecom.work",
"is_leader_in_dept": [1, 0],
"direct_leader": ["lisi", "wangwu"],
"avatar": "http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0",
"thumb_avatar": "http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/100",
"telephone": "020-123456",
"alias": "jackzhang",
"address": "广州市海珠区新港中路",
"open_userid": "xxxxxx",
"main_department": 1,
"extattr": {
"attrs": [
{
"type": 0,
"name": "文本名称",
"text": {
"value": "文本"
}
},
{
"type": 1,
"name": "网页名称",
"web": {
"url": "http://www.test.com",
"title": "标题"
}
}
]
},
"status": 1,
"qr_code": "https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx",
"external_position": "产品经理",
"external_profile": {
"external_corp_name": "企业简称",
"wechat_channels": {
"nickname": "视频号名称",
"status": 1
},
"external_attr": [
{
"type": 0,
"name": "文本名称",
"text": {
"value": "文本"
}
},
{
"type": 1,
"name": "网页名称",
"web": {
"url": "http://www.test.com",
"title": "标题"
}
},
{
"type": 2,
"name": "测试app",
"miniprogram": {
"appid": "wx8bd80126147dFAKE",
"pagepath": "/index",
"title": "my miniprogram"
}
}
]
}
}
若使用普通的提取方式会很麻烦:
场景 | 方式 |
---|---|
提取 errcode 对应的值 |
res["errcode"] |
提取 title 对应的值 |
res["extattr"]["external_profile"]["external_attr"][1]["web"]["title"] |
针对于以上这个问题,就可以使用 JSONPath 工具解决:
- 在 JSON 数据中定位和提取特定信息的查询语言。
- JSONPath 使用类似于 XPath 的语法,使用路径表达式从 JSON 数据中选择和提取数据。
- 相比于传统的提取方式,更加灵活,并且支持定制化。
如果使用 JSONPath,同样做数据提取:
场景 | 对应实现 | JSONPath 实现 |
---|---|---|
提取 errcode 对应的值 |
res["errcode"] |
$.errcode |
提取 title 对应的值 | res["extattr"]["external_profile"]["external_attr"][1]["web"]["title"] 等 |
$..title |
JSONPath表达式
JSONPath
通过表达式定位 JSON 中的数据,一共有15个表达式,通过它们的灵活组合,提取 JSON 中数据非常容易。 JSON 对象中每一个 key 都叫做一个属性(property)。下面是所有的 JSONPath 表达式,及其含义。
表达式 | 含义 |
---|---|
$ |
表示JSON对象的根节点 |
.property |
表示在某个对象的属性 |
property |
表示在某个对象的属性,.property作用一样,当property包含点号时就得用这种形式 |
[n] |
表示一个数组中的下标为n的元素 |
[index1,index2,..] |
表示一个数组中的下标为index1,index2,...的元素 |
..property |
递归查找所有属性名为property的值,返回一个list |
* |
通配符,匹配所有的属性名 |
[start:end][start:] |
表示一个数组中的下标从start到end的元素,不含end,返回一个list |
[:n] |
表示一个数组最开始的n个元素 |
[-n:] |
表示一个数组最后的n个元素 |
[?(@expression) ] |
表示一个过滤器,expression是过滤条件 |
在断言中的应用
依然假定上面的 JSON
对象是某个接口的 response
反序列化得到的。接下来,看看在自动化测试中如何利用 JSONPath
表达式进行断言。
在 Python 语言中,使用 JSONPath
需要安装一个第三方库 jsonpath
。
pip install jsonpath
假定这是一个接口的 response
response={ "store": {
"book": [
{ "category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{ "category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{ "category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{ "category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
},
"expensive": 10
}
- 测试用例1:
验证 response 中包含 Nigel Rees
, Evelyn Waugh
, Herman Melville
, J. R. R. Tolkien
这四位作者的图书。
import jsonpath
def test_authors():
author_list = jsonpath.jsonpath(response, '$.store.book[*].author')
assert author_list == ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien']
验证 response 中的商品价格包含8.95, 12.99, 8.99, 22.99, 19.95这5种。
def test_all_price():
store_price_list = jsonpath.jsonpath(response, '$.store..price')
# store_price_list = jsonpath.jsonpath(response, '$..price')
assert store_price_list == [8.95, 12.99, 8.99, 22.99, 19.95]
验证 response 中的第3本图书的书名是 Moby Dick。
def test_third_book_title():
book_3 = jsonpath.jsonpath(response, '$.store.book[2]')
assert book_3[0]['title'] == "Moby Dick"
验证 response 中的最后一本图书的 isbn
是 0-395-19395-8。
def test_last_book_isbn():
last_book_isbn = jsonpath.jsonpath(response, f'$.store.book[-1:]')
assert last_book_isbn[0]['isbn'] == "0-395-19395-8"
验证 repsonse 中前两本书价格之和是8.95 + 12.99
def test_12_books_price():
book_12 = jsonpath.jsonpath(response, '$..book[0,1].price')
assert book_12[0] + book_12[1] == 8.95 + 12.99
验证 response 中有两本书的价格小于10
def test_2books_cheap_than_10():
book_lg10 = jsonpath.jsonpath(response, '$..book[?(@.price<10)]')
assert len(book_lg10) <= 2
验证 response
中具有 color
属性的商品,其 color
是 red
def test_has_color():
colorful_goods = jsonpath.jsonpath(response, '$.store..[?(@.color)]')
assert "red" == colorful_goods[0]['color']
练习
网站:高德开放平台
对网站内的接口进行JSON响应断言,具体断言什么,可以根据自己返回的结果断言(例如:b 接口对 a 接口的 id
参数有依赖,那么就可以对 a 接口返回的 id
参数进行断言)
假设的例子:
import requests
import pprint
def test_res_json():
r = requests.get("https://httpbin.ceshiren.com/get")
pprint.pprint(r.json())
assert r.status_code == 200
assert r.json()["headers"]["Host"] == "httpbin.ceshiren.com"
#断言args是否一个空字典
assert r.json()["args"] == {}
test_res_json()
练习(Java)
演练环境:httpbin.org
断言方式:
- 直接断言:
then().body()
。 - 提取后断言:
then().extract().path()
。
直接断言:
then().body()
:
- 结合
hamcrest
使用。 body("想要提取的信息", 预期结果操作)
。- 使用
gpath
语法提取(了解)。
常用 hamcrest
示例:
应用场景 | 对应方法 |
---|---|
是否相等 | equalTo() |
是否包含 | hasItems() |
注意: 这些静态方法都来自于 org.hamcrest.Matchers
package com.ceshiren.res.json;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
/**
* 使用RestAssured进行JSON响应体的断言测试
*/
public class TestAssertJson {
/**
* 测试GET请求后响应体中的某个字段值是否与预期相符
*/
@Test
void jsonBody(){
given() // 构建HTTP请求
.when() // 发送请求
.get("https://httpbin.hogwarts.ceshiren.com/get") // GET请求URL
.then() // 验证响应
.log().all() // 打印完整的响应信息
// 结合hamcrest断言响应体中"origin"字段的值
.body("origin", equalTo("36.112.118.254, 182.92.156.22")); // 断言"origin"字段的值
}
/**
* 测试GET请求后响应体中的某个字段值是否与预期相符
*/
@Test
void doubleBody(){
given() // 构建HTTP请求
.when() // 发送请求
.get("https://httpbin.hogwarts.ceshiren.com/get") // GET请求URL
.then() // 验证响应
.log().all() // 打印完整的响应信息
// 结合hamcrest断言响应体中"headers.Host"字段的值
.body("headers.Host", equalTo("httpbin.hogwarts.ceshiren.com")); // 断言"headers.Host"字段的值
}
/**
* 测试POST请求后响应体中的数组元素是否包含特定值
*/
@Test
void extractArray(){
String jsonData = "{\"username\":\"hogwarts\",\"password\":\"test12345\",\"code\":[1,2,3]}"; // JSON请求体数据
given() // 构建HTTP请求,设置请求体
.body(jsonData)
.when() // 发送请求
.post("https://httpbin.hogwarts.ceshiren.com/post") // POST请求URL
.then() // 验证响应
.log().all() // 打印完整的响应信息
// 结合hamcrest断言响应体中"json.code"数组包含特定值
.body("json.code", hasItem(1)); // 断言数组中包含值1
}
}
提取后断言:
then().extract().path()
:
extract()
: 提取方法,返回值为Response
。path()
: 从返回值中提取想要的信息(使用gpath
语法)。
package com.ceshiren.res.json;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* 使用RestAssured从响应中提取JSON数据的测试类
*/
public class TestExtractJson {
/**
* 测试从嵌套的JSON响应中提取特定字段的值
*/
@Test
void extractJson() {
String jsonData = "{\"username\":\"hogwarts\",\"password\":\"test12345\",\"code\":[1,2,3]}"; // JSON请求体数据
Response response = (Response) given() // 构建HTTP请求,设置请求体
.body(jsonData)
.when() // 发送请求
.post("https://httpbin.hogwarts.ceshiren.com/post") // POST请求URL
.then() // 验证响应并提取数据
.log().all() // 打印完整的响应信息
.extract(); // 提取响应体
// 从响应中提取"json.username"字段的值
String username = response.path("json.username");
// 打印提取到的用户名
System.out.printf("提取内容为:%s\n", username);
// 断言提取到的用户名是否与预期相符
assertEquals("hogwarts", username);
}
/**
* 测试从JSON数组中提取特定索引的元素
*/
@Test
void extractArray() {
String jsonData = "{\"username\":\"hogwarts\",\"password\":\"test12345\",\"code\":[1,2,3]}"; // JSON请求体数据
// 构建HTTP请求,设置请求体,发送请求,验证响应并提取数据
Integer code = given()
.body(jsonData)
.when()
.post("https://httpbin.hogwarts.ceshiren.com/post")
.then()
.log().all()
.extract().path("json.code[0]"); // 提取"json.code"数组中索引为0的元素
// 打印提取到的code值
System.out.println("提取到的code:" + code);
// 断言提取到的code是否与预期相符
assertEquals(1, code);
}
}
总结
- 接口响应体的概念。
- 对
JSON
类型的响应体完成断言。