Wady(绿洲)Java SDK单元测试从0到1的突破
From 智能化产品测试团队
# 1 Java SDK单元测试
智能化算法产品团队于2020年初,正式承接智能算法Java SDK的测试,拉开了我们在白盒测试领域探索与实践的序幕。
经过选型测试框架、规范测试流程、打通测试环境等方面的不断落地,使得我们在上半年快速具备了Java SDK的基本测试能力,能够满足SDK接口的功能、性能、以及围绕算法效果的测试。
在今年九月份,我们通过对TestNG框架进行二次开发,重点增强了其数据驱动能力,使得SDK单元测试代码编写成本降低50%,同时通过设计实现代码与数据的解耦,提升了用例的可维护性。
至此,Wady(绿洲)Java SDK 单元测试框架正式成型(如下图,黄色框部分),并投入到实际测试当中。

这里,我们将更多的从技术角度,介绍Wady的在单元测试中的设计优化与实际应用。
# 1.1 什么是SDK
SDK即“软件开发工具包”,一般是一些被软件工程师用于为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件的开发工具的集合。通俗点是指由第三方服务商提供的实现软件产品某项功能的工具包。
通常SDK是由专业性质的公司提供专业服务的集合,比如提供安卓开发工具、或者基于硬件开发的服务等。也有针对某项软件功能的SDK,如推送技术、图像识别技术、移动支付技术、语音识别分析技术等。开发者不需要再对产品的每个功能进行开发,选择合适稳定的SDK服务并花费很少的精力就可以在产品中集成某项功能。
# 1.2 什么是Wady
智能化算法测试团队基于Java SDK测试场景提供的统一单元测试框架—--—Wady,其一定程度解决以下几类问题:
- 测试数据与代码耦合程度较高,用例的可扩展性和可复用性较低。
- 测试覆盖度的提高与测试用例维护成本增长之间的线性关系。
- 缺乏灵活统一的期望比对方式,存在较高的用例开发成本(重复度高)。
# 2 Wady具备哪些特性
# 2.1 新特性
数据驱动,基于Yaml 实现测试用例与测试数据的解耦。
参数化,支持指定参数项的参数化,实现参数的排列组合、顺序组合,为低维护成本下实现高覆盖提供可能,除了能够实现入参的参数化,也支持了对期望结果的参数设置。
提供统一且唯一的数据驱动方法,支持根据测试类、测试方法在指定目录下自动加载对应的配置文件。
提供统一的测试用例入参格式,全局唯一入参格式。
提供统一灵活的结果比对方法,支持JsonObject、JsonArray、String、Integer等基本数据类型的精确、模糊比较。
# 2.2 即将支持的特性
- 接口测试用例自动生成,通过解析Jar文件,自动生成各接口的测试代码,实现对测试代码依赖解耦,将代码开发、维护成本降至到零。
- Yaml配置文件自动生成,根据接口方法入参,生成测试数据配置文件,实现驱动测试。
# 3 更深入的了解Wady特性
# 3.1 关于单元测试的数据驱动配置
关于数据驱动配置,一图抵千言,如下:

# 3.1.1 数据驱动特性
YAML语言的设计参考了JSON、XML和SDL等语言。YAML 强调以数据为中心,简洁易读,编写简单,YAML基本格式要求,如下:
- 大小写敏感
- 通过缩进表示层级关系
- 禁止使用
tab缩进,只能使用空格键 - 缩进的空格数目不重要,只要相同层级左对齐即可
- 使用
#表示注释
借助YAML多文档块特性强化TestNG数据驱动
在对某些方法进行测试时,我们通常会使用不同的数据对方法进行覆盖,如 边界值测试、等价类测试,YAML多文档块(---)实现了在一个yaml中,隔离不同测试数据的目的,即可通过一个YAML文件的多个配置块驱动一个测试方法执行。
---
# 用例描述
testcase: 验证 XXX 功能是否符合预期
# 参数配置
parameter:
jsonObjecta: {"Id":"1","code":"Connect","name":"连接","sentenceDesc":"","type":"DEVICE","grade":[1,2,3,4]}
# 期望配置
expectResult:
expect: {"Id":"2","code":"Connect","name":"连接","sentenceDesc":"","type":"DEVICE","grade":[1,2,3,4]}
---
# 用例描述
testcase: 验证 XX 功能是否符合预期
# 参数配置
parameter:
jsonObjecta: {"Id":"3","code":"Connect","name":"连接","sentenceDesc":"","type":"DEVICE","grade":[1,2,3,4]}
# 期望配置
expectResult:
expect: {"Id":"3","code":"Connect","name":"连接","sentenceDesc":"","type":"DEVICE","grade":[1,2,3,4]}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3.1.2 数据驱动引入参数化
参数化是对数据驱动的进一步加强,帮助我们最大程度的减少数据配置成本,如我们需要进行参数组合覆盖时,需要通过文档块逐一编写测试数据配置,重复性很高,随着配置的不断增多,维护成本也剧增。
参数化特性实现了参数灵活的排列组合和顺序排列。
# 3.1.2.1 排列组合
{"Id":"244","code":"Connect","sentenceDesc":{"desc":"","sentence":"你好"},"grade":[1,2,3,4]}
如,我们根据测试用例覆盖需要对以上参数中的desc、grade中的第0个元素,进行不同参数组合的覆盖,通过在配置文件中添加parameterization配置项即可,如下:
---
testcase: 验证 XX 功能是否符合预期
# 参数化配置
parameterization:
jsonObjectDemo.sentenceDesc.desc: 数据驱动, 参数化
jsonObjectDemo.grade[0]: 99,88,77
# 参数组合方式,非必填,支持顺序和排列组合,默认排列组合
parameterCombination: permutation
# 参数配置
parameter:
jsonObjectDemo: {"Id":"244","code":"Connect","sentenceDesc":{"desc":"","sentence":"你好"},"grade":[59, 33]}
# 期望配置
expectResult:
expect: {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
如上参数化配置,当我们选择排列组合(parameterCombination: permutation)时,用例运行时,将数据配置拆解为以下6种参数组合,并回填至原始参数中,驱动同一个测试用例执行,拆分后的数据配置如下:
第1组 参数组合:
$.jsonObjectDemo.sentenceDesc.desc:数据驱动
$.jsonObjectDemo.grade[0]:99
第1组 参数组合回填原始参数:
{"jsonObjectDemo":{"code":"Connect","grade":["99",33],"sentenceDesc":{"sentence":"你好","desc":"数据驱动"},"Id":"244"}}
第2组 参数组合:
$.jsonObjectDemo.sentenceDesc.desc:数据驱动
$.jsonObjectDemo.grade[0]:88
第2组 参数组合回填原始参数:
{"jsonObjectDemo":{"code":"Connect","grade":["88",33],"sentenceDesc":{"sentence":"你好","desc":"数据驱动"},"Id":"244"}}
第3组 参数组合:
$.jsonObjectDemo.sentenceDesc.desc:数据驱动
$.jsonObjectDemo.grade[0]:77
第3组 参数组合回填原始参数:
{"jsonObjectDemo":{"code":"Connect","grade":["77",33],"sentenceDesc":{"sentence":"你好","desc":"数据驱动"},"Id":"244"}}
第4组 参数组合:
$.jsonObjectDemo.sentenceDesc.desc:参数化
$.jsonObjectDemo.grade[0]:99
第4组 参数组合回填原始参数:
{"jsonObjectDemo":{"code":"Connect","grade":["99",33],"sentenceDesc":{"sentence":"你好","desc":"参数化"},"Id":"244"}}
第5组 参数组合:
$.jsonObjectDemo.sentenceDesc.desc:参数化
$.jsonObjectDemo.grade[0]:88
第5组 参数组合回填原始参数:
{"jsonObjectDemo":{"code":"Connect","grade":["88",33],"sentenceDesc":{"sentence":"你好","desc":"参数化"},"Id":"244"}}
第6组 参数组合:
$.jsonObjectDemo.sentenceDesc.desc:参数化
$.jsonObjectDemo.grade[0]:77
第6组 参数组合回填原始参数:
{"jsonObjectDemo":{"code":"Connect","grade":["77",33],"sentenceDesc":{"sentence":"你好","desc":"参数化"},"Id":"244"}}
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
Wady与原生TestNG覆盖同样的测试点,数据驱动配置数量差异如下:

# 3.1.2.2 顺序排列
排列组合的方式适合与参数之间不存在关联关系的场景,若当上述例子中desc,grade存在关联关系时,我们可以使用顺序组合的方式,如下:
---
testcase: 验证 XX 功能是否符合预期
# 参数化配置
parameterization:
jsonObjectDemo.sentenceDesc.desc: 数据驱动,参数化
jsonObjectDemo.grade[0]: 99,88,77
# 参数组合方式,非必填,支持顺序和排列组合,默认排列组合
parameterCombination: sequential
# 参数配置
parameter:
# 参数配置
jsonObjectDemo: {"Id":"244","code":"Connect","sentenceDesc":{"desc":"","sentence":"你好"},"grade":[59, 33]}
# 期望配置
expectResult:
expect: {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
当我们选择 顺序组合(parameterCombination: sequential) 时,每组参数取相同索引位置的元素进行组合。如上数据配置可生成以下3种参数组合,并回填至原始参数中,如下:
第1组 参数组合:
jsonObjectDemo.sentenceDesc.desc:数据驱动
jsonObjectDemo.grade[0]:99
第1组 参数组合回填原始参数:
{"jsonObjectDemo":{"code":"Connect","grade":["99",33],"sentenceDesc":{"sentence":"你好","desc":"数据驱动"},"Id":"244"}}
第2组 参数组合:
jsonObjectDemo.sentenceDesc.desc:参数化
jsonObjectDemo.grade[0]:88
第2组 参数组合回填原始参数:
{"jsonObjectDemo":{"code":"Connect","grade":["88",33],"sentenceDesc":{"sentence":"你好","desc":"参数化"},"Id":"244"}}
第3组 参数组合:
jsonObjectDemo.grade[0]:77
第3组 参数组合回填原始参数:
{"jsonObjectDemo":{"code":"Connect","grade":["77",33],"sentenceDesc":{"sentence":"你好","desc":""},"Id":"244"}}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
另外在这顺序组合的场景下,也支持了对期望结果的参数化设置,使得实现期望结果与入参保持一定的关联性,如下
# 参数化配置
parameterization:
jsonObjectDemo.sentenceDesc.desc: 入参参数化1,入参参数化2
jsonObjectDemo.grade[0]: 99,88,77
expect.sentenceDesc.desc: 期望结果参数化1,期望结果参数化2
# 参数组合方式,非必填,支持顺序和排列组合,默认排列组合
parameterCombination: sequential
# 参数配置
parameter:
# 参数配置
jsonObjectDemo: {"Id":"244","code":"Connect","sentenceDesc":{"desc":"","sentence":"你好"},"grade":[59, 33]}
# 期望配置
expectResult:
expect: {"Id":"244","code":"Connect","sentenceDesc":{"desc":"描述","sentence":"你好"},"grade":[77, 33]}
2
3
4
5
6
7
8
9
10
11
12
13
14
根据上述配置,我们不仅可以将每组参数取相同索引位置的元素进行组合,也一同实现了相同所以位置的期望结果的组合。需要注意的是,期望结果的参数化,建议只应用于当parameterCombination为sequential时。
# 3.1.3 更加灵活的结果比对方式
为满足不同场景的结果验证需求,Wady 支持三种维度的验证如下:
- 校验精度:精确(
precision)、模糊(fuzzy) - 校验方式:排除(
exclude)、选择(choose)、全部(默认) - 校验范围:指定
key、全覆盖(默认)
当我们有这些需求时,只需要在配置文件中添加check配置项即可,如下:
# 校验方式,支持精确比较和非空校验,非必填
check:
# 校验精度,支持精确校验、模糊经验,默认精准,非必填
precision: precision
# 校验范围,默认全校验,非必填
key: sentenceDesc.desc
# 校验方式:选择校验、过滤校验、默认全校验,非必填
way: choose
2
3
4
5
6
7
8
# 3.2 关于单元测试的用例代码
单元测试用例,一图抵千言,如下:

# 3.2.1 统一的数据驱动方法
实现AbstractAiTestFramework类提供的统一数据驱动方法(TestDataProvider),该方法根据实现根据类名、方法名在指定yaml文件目录下(存放数据驱动数据)加载对应的yaml文件中测试数据,同时当无其对应的yaml文件时,会自动创建该文件。TestDataProvider根据yaml中多文档快的数据隔离的特性,使用不同块依次驱动测试,实现数据驱动,如下:
package frameworkTest;
import framework.factory.AbstractAiTestFramework;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.util.Map;
public class CompareJsonTest extends AbstractAiTestFramework {
@Test(dataProvider = "TestDataProvider")
public void compareJsonObjectTest(Map<String, Object> parameter){
Assert.assertNotNull();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 3.2.2 统一的配置获取方式
Wady通过com.alibaba.fastjson.JSONPath灵活获取配置中具体的内容,如:
// 获取参数
JSONObject methodParameter = (JSONObject) JSONPath.eval(paramsObj,"$.parameter.jsonObjecta");
// 方法调用, 此处省略. 假设 methodParameter 同样作为方法返回结果.
...
// 获取期望结果
JSONObject expectResult = (JSONObject) JSONPath.eval(paramsObj,"$.expectResult.expect");
2
3
4
5
6
7
8
# 3.2.3 统一的结果比对方式
我们通过CompareJsonUtils.compareJson方法中传入实际结果、期望结果以及yaml配置信息,即可根据yaml中配置的方式实现灵活的比较。降低对测试脚本的人工干预,为后续自动生成测试脚本提供可能。
// 统一的结果比对接口, 根据配置实现即可灵活选择、过滤比对方式 及精确、模糊校验角度.
CompareBaseResult compareBaseResultDTO = CompareJsonUtils.compareJson(methodParameter, expectResult, paramsObj);
2
因为Wady实现了精确比较,并且记录了不一致的个数,因此我们在断言时,仅需断言不一致个数是否为0即可,如下:
// 结果断言
Assert.assertEquals(compareBaseResultDTO.getRetCode(), 0,String.valueOf(compareBaseResultDTO.getRetValue()));
2
当实际结果与期望结果存在不一致的情况时,支持输出每个细节的比对结果,并且将不一致的详细信息记录下来,同时输出不一致的具体信息,如下:

Wady与原生TestNG覆盖同样的测试点,单元测试代码量差异如下:

# 4 致谢
感谢以下同学,在整体框架设计、优化、应用过程中的努力付出,以及提出的宝贵意见,以下排名不分先后:
姜越、郭群、张天宇、马杰