开源测试工具 专为测试人员打造的 json 解析 jar 包--zson

再见理想 · 2017年03月13日 · 最后由 cc 回复于 2019年03月13日 · 3027 次阅读

前言

现在大家都认可 Python 在接口测试方面效率比较高,究其原因,可能是 Python 的请求库功能强大,但 Java 的 HttpClient 封装得好的话,也可以一句代码发送请求,还有一点,Java 的 TestNg 我个人认为是一个非常强大的测试框架,Python 中的那些测试框架应该没有与之比肩的,但即便始此,Java 在接口测试上还是举步维艰,这是因为在请求后对结果的处理,Python 天然支持 json 解析,而 Java 呢?得依靠第三方库,且解析取值得一大片代码,更见鬼的是这一大片代码是毫无复用性可言,更有甚者,在解析时会搞一个 pojo 文件,更让 Python 者觉得用 Java 简直是灾难。

为了解决测试人员在 Java 对 json 解析的困惑,zson 就应运而生了。因为我本人做过 UI 自动化测试,对 XPATH 有一定的了解,所以 zson 对 json 的操作中加入了一个类似于 xpath 的路径的概念,即利用一个路径来操作 json 串。如果一个 json 串有非常复杂的层级关系,如果想获取最里面的某个 key 的值,正常情况下那就得一层一层的解析进去,非常的繁琐,如果用 zson,只需要一句代码,给定一个路径(值得注意的是,也可以是相对路径哦),就可以获取到对应的值,这样可以大大的提高生产力。

zson

#### 专为测试人员打造的 JSON 解析器

当然,有很多很好的 JSON 解析的 JAR 包,比如 fastjson,GSON,甚至也有为我们测试人员而打造的 JSONPATH,但我还是自已实现了一下,也不是没事造轮子,因为我是站在测试人员的立场来设计及实现这个工具的。其主要特点是用一个类似于 xpath 的选择器来获取相应的值。


#### 特点

  • 无需层层解析
  • 根据给定的路径 (类 XPATH 路径) 来获取相应的值
  • 支持相对路径

使用场景

设定一个 json 串:

{
    "retCode": "200",
    "retMsg": "success",
    "data": [
        {
            "id": 1,
            "name": "test",
            "date": "2017-01-09 13:30:00"
        },
        {
            "id": 2,
            "name": "test1",
            "date": "2017-01-09 13:40:00"
        }
    ]
}

如果想要获取以上 json 串的所有"name"的值,对于正常解析,你得遍历,但对于 zson,你只需要这样:

ZsonResult zr = ZSON.parseJson(json);
List<Object> names = zr.getValues("//name");

我们在进行结果断言时,有时候请求返回的一整个 json 串作为一个期望值来进行断言,但 json 串中往往会存在有不固定的值,比如上面 json 串的"date",每次都是变化的,这样就不好断言了,于是,在 zson 中,我们可以把这个 date 的值进行更改,改成一个固定的值:

ZsonResult zr = ZSON.parseJson(json);
zr.updateValue("//date","0000-00-00 00:00:00");

或者干脆删除这个结点:

ZsonResult zr = ZSON.parseJson(json);
zr.deleteValue("//date");

以上 zson 对 json 串的操作包含了查找,更新,删除。zson 还有对 json 串中增加一个子字符串的操作:

ZsonResult zr = ZSON.parseJson(json);
zr.addValue("/data",2,"{\"id\":3,\"name\":\"test2\",\"date\":\"2017-01-09 14:30:00\"}");

选择器 path 说明

示例一:

[
    {
        "firstName": "Eric",
        "lastName": "Clapton",
        "instrument": "guitar"
    },
    {
        "firstName": "Sergei",
        "lastName": "Rachmaninoff",
        "instrument": "piano"
    }
]

找出第二个 firstName: /*[1]/firstName
输出:Sergei


找出第一个 Map: /*[0]

输出:{"firstName": "Eric","lastName": "Clapton","instrument": "guitar"}


找出所有的 firstName: //firstName
输出:["Eric","Sergei"]


示例二:

{"a":["a"],"cb":{"a":1},"d":["a",{"a":[1,2]},{"a":2},""],"e":"b"}

路径: /d/*[1]/a
输出:[1,2]


路径: /d/*[1]/a/*[0]
输出:1


源码下载地址

https://github.com/zhangfei19841004/zson

共收到 55 条回复 时间 点赞

赞一个。 不过,个人觉得单元框架都差不多吧 就像 python 的 nose 用起来肯定比 unittest 爽 但你敢说 nose 就比 unittest 高明了?

nose 与 unittest 都是 python 系的吧?我完全不懂 python 啊。。但我个人还是认为 testng 是比较优秀的测试框架了!

😂 感谢分享。。。遇上 jmeter 不得不用 java。。。习惯敲 python 的欲哭无泪

dadeshuo 回复

jmeter 里加入 zson.jar 即可。😀

没看出来有什么特别的,超越 rest-assured + json path 就放弃吧。

卡农Lucas 回复

支持相对路径,支持对 json 串的增删改查,这应该算是一个特点吧。😃

类似的做了一个,区别点

  1. 自定义的规则用了 JsonView 展示规则
  2. 不要的字段同样定义一个参数存储,校验时不校验这些字段的 Value 值

Json 增删改查👍一个

Roc 回复

其实在解析时,把路径都生成了,且用路径做了一个索引,这样查找起来相当的快。但我没有提供出获取所有路径的 API,其实这里的路径就相当于 json schema 了。

Roc 回复

因为返回的 json 串中可能有动态的,比如里面有个时间戳,这样不方便我们做比较,所以把这个时间戳进行替换或者删除就是一个比较好的解决方案了,zson 支持!

再见理想 回复

替换吧,不建议做删除,替换可以确保有这个 key,但是替换还要准备一个初始数据,所以我是检查到这个 Key 值时直接跳过,写 case 的时候方便一些吧

作者写这个确实是方便测试的角度出发👍

Roc 回复

zson 支持!😃

simple 回复

多谢捧场!

个人觉着,这个的特点在于单测时可以对数据的 MOCK 有一定帮助
但对于 json 解析这块没啥特别之处。

飞哥来了,必须顶!d=====( ̄▽ ̄*) b

分享下自己项目中对 json 处理的一些想法。
类 xpath 获取对应的值是很方便的,平时写 Selenium 的 case 大多用 xpath,在做 REST API 测试时,处理 json 一般是有两种办法:

  • 按照 path 取值

    ObjectMapper mapper = new ObjectMapper();
    JsonNode root = mapper.readTree(s);
    int id = root.at("/data/0/id").asInt();
    String name = root.at("/data/0/name").asText();
    
  • 定义 Python/Java 对象 mapping 到 json

在实际使用中,如果 json 结构和深度非常复杂,第一种办法在组装 request body 和解析 response body 时会很繁琐,并且代码不再那么整洁,所以后来大多用第二种方法。

比如
http://h17007.www1.hpe.com/docs/enterprise/servers/oneview3.0/cic-api/en/api-docs/current/index.html#rest/server-profiles

{
    "type": "ServerProfileV6",
    "name": "Profile101",
    "serverHardwareUri": "/rest/server-hardware/{serverUUID}",
    "affinity": "Bay",
    "macType": "Virtual",
    "serialNumberType": "Virtual",
    "wwnType": "Virtual",
    "hideUnusedFlexNics":true,
    "connections": [{
        "id": 1,
        "name":"connection1",
        "functionType": "Ethernet",
        "portId": "Flb 1:1-a",
        "requestedMbps": 2500,
        "networkUri": "/rest/ethernet-networks/{networkUUID}",
        "boot": {
            "priority": "Primary"
        }
    },
    {
        "id": 2,
        "functionType": "Ethernet",
        "portId": "Auto",
        "requestedMbps": 2500,
        "networkUri": "/rest/network-sets/{networkSetUUID}",
        "boot": {
            "priority": "Secondary"
        }
    },
    {
        "id": 3,
        "functionType": "FibreChannel",
        "portId": "Auto",
        "requestedMbps": 2500,
        "networkUri": "/rest/fc-networks/{fcNetworkID}",
        "boot": {
            "priority": "Primary",
            "bootVolumeSource": "UserDefined",
            "targets": [{
                "arrayWwpn": "{arrayWwpn}",
                "lun": "{lun}"
            }]
        }
    },
    {
        "id": 4,
        "functionType": "Ethernet",
        "portId": "Auto",
        "requestedMbps": 2500,
        "macType": "UserDefined",
        "mac": "12:11:11:11:00:00",
        "networkUri": "/rest/network-sets/{networkSetUUID}",
        "boot": {
            "priority": "NotBootable"
        }
    },
    {
        "id": 5,
        "functionType": "FibreChannel",
        "portId": "Auto",
        "requestedMbps": 2500,
        "wwpnType":"UserDefined",
        "wwnn":"10:00:1C:11:00:00:00:00",
        "wwpn":"10:00:1C:11:00:00:00:01",
        "macType":"UserDefined",
        "mac":"12:11:11:00:00:00",
        "networkUri": "/rest/fc-networks/{fcNetworkID}",
        "boot": {
            "priority": "Secondary",
            "bootVolumeSource": "UserDefined",
            "targets": [{
                "arrayWwpn": "{arrayWwpn}",
                "lun": "{lun}"
            }]
        }
    }],
    "boot": {
        "manageBoot": true,
        "order": ["PXE",
        "HardDisk",
        "CD",
        "Floppy",
        "USB"]
    },
    "bios": {
        "manageBios": true,
        "overriddenSettings": [{
            "id": "91",
            "value": "1"
        },
        {
            "id": "158",
            "value": "2"
        }]
    },
    "localStorage": {
        "sasLogicalJBODs": [
        {
            "id": 1,
            "deviceSlot": "Mezz 1",
            "name": "Data Storage",
            "numPhysicalDrives": 1,
            "driveMinSizeGB": 200,
            "driveMaxSizeGB": 600,
            "driveTechnology": "SasHdd",
            "sasLogicalJBODUri": null
        },
        {
            "id": 2,
            "deviceSlot": "Mezz 1",
            "name": "Recovery Volume",
            "numPhysicalDrives": 2,
            "driveMinSizeGB": 200,
            "driveMaxSizeGB": 600,
            "driveTechnology": "SasHdd",
            "sasLogicalJBODUri": null
        }],
    "controllers": [
        {
            "deviceSlot": "Embedded",
            "mode": "RAID",
            "initialize": false,
            "importConfiguration": false,
            "logicalDrives": [
            {
                "name": "Operating System",
                "raidLevel": "RAID1",
                "bootable": true,
                "numPhysicalDrives": 2,
                "driveTechnology": null,
                "sasLogicalJBODId": null
            }]
      },
      {
            "deviceSlot": "Mezz 1",
            "mode": "RAID",
            "initialize": false,
            "importConfiguration": false,
            "logicalDrives": [
            {
                "name": null,
                "raidLevel": "RAID0",
                "bootable": false,
                "numPhysicalDrives": null,
                "driveTechnology": null,
                "sasLogicalJBODId": 1
            },
            {
                "name": null,
                "raidLevel": "RAID1",
                "bootable": false,
                "numPhysicalDrives": null,
                "driveTechnology": null,
                "sasLogicalJBODId": 2
            }]
      }]
    },
    "firmware": {
        "manageFirmware": true,
        "firmwareBaselineUri": "/rest/firmware-drivers/{fwBaselineId}",
        "forceInstallFirmware": false
    }
}
49875183 回复

主要是在解析时,根据路径就可得出值,无须层层解析进去。

ovpt 回复

可以用相对路径!这也是 zson 的一大特点!

再见理想 回复

我记得 rest 重新封装的那个 jsonpath 就是这个功能呀。。。

49875183 回复

jsonpath 是很强大,zson 是一个追赶者,所以还需要各位多多的支持一下,就算为了情怀嘛:测试人员自已写的测试工具!😃

57楼 已删除

说实话我不太喜欢情怀这个词,也没必要什么都上升到情怀和测试人员自己写的测试工具上(真正的情怀到底是什么我觉着也是大家都在想的事情)

我也只是实话实说,哪些现成工具已经实现了这些功能罢了

在我个人来看 JSON 解析对于其他工具来说没有特别之处,不过我认可修改数据格式这个功能,但具体实用场景还有待考究

49875183 回复

🙏 很中肯的意见。正如所说,为了实现对 json 串的 crud 操作,然后就不得不去实现了一个合理的数据结构,于是为了满足这个数据结构而去进行了 JSON 的解析,其实最开始这个需求是来源于接口测试平台的编写,在一个平台中,为了获取值,做一个路径选择器是比较好的方式。一环扣一环,最后,出现了 zson,也算是我为测试界做点微薄的贡献吧!😀

飞总好

南风 回复

😀 多谢支持

我非常赞同作者的精神,认可作者的态度。只是稍微提个小小小的意见,看了源码后发现代码中有大段的 if-else 嵌套,如果可以重构一下就更好了。

Nisir 回复

非学点好的意见,我曾经想过去修改,但当我发现如果用一些所谓的设计模式,务必会损耗系统资源,且当我去看了 fastjson 的源码后,更加坚定这里用 if-else 是非常好的选择,因为 fastjson 也是这样干的!但是我这里面的 if-else 的结构是可以进行修改且优化的,以后肯定会优化的。😀

再见理想 回复

首先,重构不代表就是非要使用什么设计模式不可,这完全是两个不同的概念。可以将部分功能提取出来作为一个方法,以一种更面向对象的方式展示。在主方法逻辑中可以通过调用各个方法的方式,使得代码更清晰易读,简洁明了。然后,我不是很喜欢用 “所谓的设计模式” 这个词,表明作者对设计模式不是很尊重,或者没有很好体会到它的美妙之处。我不是推崇非要使用设计模式的代码才是好的代码,但也不认可有一些人觉得别人老提设计模式就是装 x。其实设计模式更多的是一种编程的思想,是将众多优秀项目中的实践思想提取出来的结果,是前人经验的总结,确实在代码层次设计以及复用性、迁移性、维护性方面有很显著的效果,使得代码展示的更优雅。面向对象的代码更应该像说故事一样,一个方法就是一个动作,如果非常复杂,涉及到众多的分支,可以将其拆分成众多小的动作,最后以组合拳的形式展示。

再见理想 回复

说不定哪天 fastjson 重构了用了设计模式,然后注释这段以前太 chaos 了。

30楼 已删除
再见理想 回复

欢迎纯技术交流,不欢迎广告推广🕵

再见理想 回复

你还能再 low 点么,设计模式浪费系统资源?你 java 是体育老师教的么,丢 java 的脸

易寒 回复

不好意思,我刚刚开始学习 JAVA,以后我会尽量说我不会写 JAVA,免得丢了你们 JAVA 大佬的脸!😃

Nisir 回复

下一步我会重构一下,重点去学习一下设计模式,到时候还望多多指教!🙏

恒温 回复

easy,对于有硬实力的人,我是很尊敬的!🙏

再见理想 回复

谈不上指教,相互学习咯,昨天言语有点激烈,如有冒犯还请见谅。我也是初学者,因为不是 cs 专业出身,基础比较薄弱,但也比较重视打基础的工作。昨天看了你的博客,写了不少了。感觉你的 java 基础比我要好,反射、注解使用的都比较娴熟。但是提个个人的意见,我觉得很多方面你都涉猎了,但是都没有再深入去挖掘一下。

Nisir 回复

我作为一个测试人员,在代码方面确实有很多欠缺,但我在努力弥补,希望有朝一日能跟上你们的脚步!🙏

再见理想 回复

我也是测试,共勉。

再见理想 回复

{"a":["a"],"cb":{"a":1},"d":["a",{"a":[1,2]},{"a":2},""],"e":"b"}
路径: /d/[1]/a/[0]
输出:1
正确的写法应该是/d/*[1]/a/*[0] ,数组前面加 * 号,我也是看 github 才知道的,请自己发之前先校验下吧。

再见理想 回复

不用谦虚,跟他们互怼。技术上怕啥啊

konami1986 回复

是的,这个是我的错,我是直接从 GITHUB 上拷贝过来的,我再来检查一下。

再见理想 回复

这个态度是技术人应有的态度,带着这个态度,你就会进步。当然我言语也有激烈之处,还请海涵。

卡农Lucas 回复

不是怕,硬实力不够,我在技术上确实有所欠缺!

再见理想 回复

你的技术比他们高出一个珠穆朗玛峰。加油。

再见理想 关闭了讨论 03月14日 12:43
恒温 关闭了讨论 03月14日 15:20
再见理想 关闭了讨论 03月14日 15:33
再见理想 重新开启了讨论 03月14日 21:29

看了一堆评论,分享精神是需要支持的,点赞,我觉得测试就是要这种精神,创新意识很重要,不需要因为他人意见去完全改变,只吸收自己觉得好的👌

—— 来自 TesterHome 官方 安卓客户端

唉,飞哥是我们的前辈!

CC 回复

多谢支持!我写了众多的口水博客,zson 应该是我最得意之作了,抛开代码层面,解析后的数据结构才是 zson 的精髓,我花了大量的时间去思考并验证,这才是收获最大的地方!🙏

json 不是一般直接一句代码直接编译成对象的咩,为什么还需要 xpath 检查 233

心向东 回复

解析成对象后 才是根据路径选择器来操作

支持相对路径能不能举个例子,怎么用

feifei 回复

比如:
[
{
"firstName": "Eric",
"lastName": "Clapton",
"instrument": "guitar"
},
{
"firstName": "Sergei",
"lastName": "Rachmaninoff",
"instrument": "piano"
}
]
想取所有的 firstName,就用 zr.getValues("//firstName");

很有用,顶一个!

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册