在实际的接口测试时,传参有时候可能需要很多,也可能我们就是想要一份完整的参数,必填项和非必填项都包含在内,比如下面的 json

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
32
33
34
35
36
37
{
"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
}

一个个在方法中传入显然不现实;写入hashmap中传入的话工作量和复杂度也很大,这个时候就需要一个模板,把我们需要的参数和结构提前定义好,我们只需要修改其中对应的值即可,这就要引出今天的两位主角:

  • JsonPath
  • Mustache

JsonPath

参考文档

先来看第一个模板技术JsonPath,注意这里的JsonPath指的是Jayway JsonPath,maven依赖如下:

1
2
3
4
5
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.5.0</version>
</dependency>

元素定位

在UI自动化中我们会使用到xpath的元素定位的技术,根据对比,json也有JsonPath的元素定位,为了方便理解记忆,我们可以Xpath与JsonPath进行对比

Xpath JSONPath 描述
/ $ 文档根元素
. @ 当前元素
/ . or[] 匹配下级元素
.. N/A 匹配上级元素,JsonPath不支持此操作符
// .. 递归匹配所有子元素
* * 通配符,匹配下级元素
@ N/A 匹配属性,JsonPath不支持此操作符
[] [] 下标运算符,根据索引获取元素,XPath索引从1开始,JsonPath索引从0开始
` ` [,]
N/A [start:end:step] 数据切片操作,XPath不支持
[] ?() 过滤表达式
N/A () 脚本表达式,使用底层脚本引擎,XPath不支持
() N/A 分组,JsonPath不支持

注意:

  • JsonPath的索引从0开始计数
  • JsonPath中字符串使用单引号表示,例如:$.store.book[?(@.category=='reference')]中的’reference’

jsonPath的语法要点:

  • $ 表示文档的根元素
  • @ 表示文档的当前元素
  • .node_name 或 [‘node_name’] 匹配下级节点
  • [index] 检索数组中的元素
  • [start:end:step] 支持数组切片语法
  • * 作为通配符,匹配所有成员
  • .. 子递归通配符,匹配成员的所有子元素
  • (<expr>) 使用表达式
  • ?(<boolean expr>)进行数据筛选

工具类封装

JsonPathUtils.java

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.api.object.utils;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

/**
* @author jingLv
* @date 2021/01/06
*/
public class JsonPathUtils {

private static final Logger logger = LoggerFactory.getLogger(JsonPathUtils.class);

/**
* 文件地址
*/
private final String path;
/**
* jsonpath对应修改的内容
*/
private final Map<String, Object> content;

public JsonPathUtils(String path, Map<String, Object> content) {
this.path = path;
this.content = content;
}

/**
* 解析json,使用jsonpath语法获取值进行替换
*
* @return 返回已替换的json
*/
public String modifyResult() {
logger.info("读取JSON文件:{},替换的内容:{}", this.path, JSONUtil.parse(content));
DocumentContext documentContext = JsonPath.parse(this.getClass().getResourceAsStream(this.path));
for (Map.Entry<String, Object> entry : this.content.entrySet()) {
documentContext.set(entry.getKey(), entry.getValue());
}
return documentContext.jsonString();
}
}

示例演示

以下实操演示均先分为以下三步:

  1. Jayway JsonPath的parse方法对json文件内容进行解析获取
  2. 再使用set方法以Jayway JsonPath的语法对解析结果进行修改
  3. 最后使用json工具对结果进行格式化输出,查看修改结果

示例1:$.store.book[*].author ——所有书的作者,将所有书的作者名都改为”测试作者”

1
2
3
4
5
6
7
8
9
10
/**
* $.store.book[*].author ——所有书的作者
*/
@Test
void modifyResult() {
Map<String, Object> content = new HashMap<>();
content.put("$.store.book[*].author", "测试作者");
String result = new JsonPathUtils("/json/book.json", content).modifyResult();
System.out.println(JSONUtil.parseObj(result));
}

执行完成,得到结果

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
32
{
"store": {
"bicycle": {
"color": "red",
"price": 19.95
},
"book": [{
"author": "测试作者",
"title": "Sayings of the Century",
"price": 8.95,
"category": "reference"
}, {
"author": "测试作者",
"title": "Sword of Honour",
"price": 12.99,
"category": "fiction"
}, {
"author": "测试作者",
"isbn": "0-553-21311-3",
"title": "Moby Dick",
"price": 8.99,
"category": "fiction"
}, {
"author": "测试作者",
"isbn": "0-395-19395-8",
"title": "The Lord of the Rings",
"price": 22.99,
"category": "fiction"
}]
},
"expensive": 10
}

示例2:$.store.*——store下所有的都进行修改,这里包括了bicycle和book。将store下的所有内容改为all change

1
2
3
4
5
6
7
8
9
10
/**
* $.store.*——store下所有的都进行修改,这里包括了bicycle和book。将store下的所有内容改为all change
*/
@Test
void modifyResult02() {
Map<String, Object> content = new HashMap<>();
content.put("$.store.*", "all change");
String result = new JsonPathUtils("/json/book.json", content).modifyResult();
System.out.println(JSONUtil.parseObj(result));
}

执行完成,得到结果

1
2
3
4
5
6
7
{
"store": {
"bicycle": "all change",
"book": "all change"
},
"expensive": 10
}

示例3:$..book[0,1]——book下的第一个和第二个,将book下的第一个和第二个内容改为first two change

1
2
3
4
5
6
7
8
9
10
/**
* $…book[0,1]——book下的第一个和第二个,将book下的第一个和第二个内容改为first two change
*/
@Test
void modifyResult03() {
Map<String, Object> content = new HashMap<>();
content.put("$..book[0,1]", "first two change");
String result = new JsonPathUtils("/json/book.json", content).modifyResult();
System.out.println(JSONUtil.parseObj(result));
}

执行完成,得到结果

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
{
"store": {
"bicycle": {
"color": "red",
"price": 19.95
},
"book": [
"first two change",
"first two change",
{
"author": "Herman Melville",
"isbn": "0-553-21311-3",
"title": "Moby Dick",
"price": 8.99,
"category": "fiction"
}, {
"author": "J. R. R. Tolkien",
"isbn": "0-395-19395-8",
"title": "The Lord of the Rings",
"price": 22.99,
"category": "fiction"
}
]
},
"expensive": 10
}

Mustache

mustache官方文档

image-20210106151554436

引出这张图,还为了说明一点,可以看出Mustache支持各种语言,常用的几乎都能囊括其中

概述

Mustache属于无逻辑模板引擎,因为其不支持if-else和for语句,主要是有双花括号括起来的模板变量及包含模板数据的模型对象组成,因为双括号看起来像胡子,因此得名mustache。

环境:java8 + maven,引入相应的依赖

1
2
3
4
5
6
<!--mustache模板框架-->
<dependency>
<groupId>com.github.spullara.mustache.java</groupId>
<artifactId>compiler</artifactId>
<version>0.9.7</version>
</dependency>

工具类封装

  • MustacheUtils.java
  • MustacheFactory 在类路径下搜索模板文件,我们的模板文件在src/main/resources路径下。
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.api.object.utils;

import cn.hutool.json.JSONUtil;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.StringWriter;

/**
* 根据对应的对象和模板生成对应的数据
*
* @author jingLv
* @date 2020/04/15
*/
public class MustacheUtils {

private static final Logger logger = LoggerFactory.getLogger(MustacheUtils.class);

/**
* mustache文件路径,在resources目录下
*/
private final String path;
/**
* 模板内容对应的对象 apimodel下
*/
private final Object model;

public MustacheUtils(String path, Object model) {
this.path = path;
this.model = model;
}

/**
* 根据模板及模板对象,指定的值进行替换
*
* @return 返回已替换的内容
*/
public String execute() {
logger.info("读取模板文件:{},替换的内容:{}", this.path, JSONUtil.parse(model));
StringWriter writer = new StringWriter();

MustacheFactory mf = new DefaultMustacheFactory();
Mustache mustache = mf.compile(this.path);
try {
mustache.execute(writer, this.model).flush();
} catch (IOException e) {
logger.error("模板内容解析异常:{}", e.getMessage());
}
return writer.toString();
}
}

示例演示

首先,在需要修改的文件中,将要修改的字段用双花括号{{变量名}}的形式表示,例如这里我们将book下第一个商品的author和price进行变量化,在resources目录下创建book.mustache文件

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
32
33
34
35
36
37
{
"store": {
"book": [
{
"category": "reference",
"author": "{{author}}",
"title": "Sayings of the Century",
"price": {{price}}
},
{
"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
2
3
4
5
6
7
8
@Test
void execute01() {
Map<String, Object> map = new HashMap<>();
map.put("author", "mustacheAuthor");
map.put("price", 56.8f);
String s = new MustacheUtils("mustache/book.mustache", map).execute();
System.out.println(JSONUtil.parse(s));
}

执行结果:

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
32
{
"store": {
"bicycle": {
"color": "red",
"price": 19.95
},
"book": [{
"author": "mustacheAuthor",
"title": "Sayings of the Century",
"price": 56.8,
"category": "reference"
}, {
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99,
"category": "fiction"
}, {
"author": "Herman Melville",
"isbn": "0-553-21311-3",
"title": "Moby Dick",
"price": 8.99,
"category": "fiction"
}, {
"author": "J. R. R. Tolkien",
"isbn": "0-395-19395-8",
"title": "The Lord of the Rings",
"price": 22.99,
"category": "fiction"
}]
},
"expensive": 10
}

Mustache不仅仅支持JSON文件的模板,还支持非JSON文件的模板,下面就以HTML文件模板介绍:

新建一个简单的模板,在resources文件下,创建文件mustache/todo.mustache,内容如下:

1
2
3
<h2>{{title}}</h2>
<small>Created on {{createdOn}}</small>
<p>{{text}}</p>

{{}}中的模板变量可以是Java类的方法和属性,也是Map对象的key。

提供模板数据是Todo类的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.mustache.demo.entity;

import lombok.Data;

import java.util.Date;

/**
* @author jingLv
* @date 2020-04-14 10:52 PM
*/
@Data
public class Todo {
private String title;
private String text;
private boolean done;
private Date createdOn;
private Date completedOn;
}

执行模板生成HTML内容的代码为:

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
32
package com.test;

import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import com.test.pojo.Todo;

import java.io.IOException;
import java.io.StringWriter;
import java.util.Date;

/**
* @author jingLv
* @date 2021/01/06
*/
public class Run {
public static void main(String[] args) throws IOException {
// 实例赋值
Todo todo = new Todo();
todo.setTitle("Todo1");
todo.setText("第一个模板");
todo.setCreatedOn(new Date());
//编译
MustacheFactory mf = new DefaultMustacheFactory();
Mustache m = mf.compile("mustache/todo.mustache");

StringWriter writer = new StringWriter();
m.execute(writer, todo).flush();
String html = writer.toString();
System.out.println(html);
}
}

输出的结果:

image-20210106141721434

优缺点对比

如果实践了上面的两种模板方法会发现两者在使用上的优缺点:

优点:

  • JsonPath:可设置默认值,在json文件中可提前设置好默认值,字段不修改就使用默认值,需要修改再进行修改
  • Mustache:不受文件格式的影响,字段替换逻辑也简单清晰

缺点:

  • JsonPath:只适用于json文件
  • Mustache:灵活度不如JsonPath,提前必须指定好需要修改的字段

总结

如果接口的请求体都是以json为主的,那么我个人推荐使用JsonPath,更灵活的控制接口参数的修改程度;当遇到非json格式的文件时可用Mustache来解决模板化的问题,只是灵活度要稍稍下降一点了可能。