一.为什么要画流程控制图?
首先看一个有些复杂的java的方法(对我来说)。
public BaseResult<PageInfo<ProjectSynDto>> queryProject(@Valid ProjectInfoQueryDTO query) {
List<ProjectSynDto> list = Lists.newArrayList();
Integer pageSize = query.getPageSize();
Integer pageNum = query.getPageNum();
String key = this.getKey(query.getCode());
BaseResult<PageInfo<ProjectSynDto>> result = this.preHandle(pageSize, pageNum, key);
if (result != null) {
return result;
}
query.setPageNum(1);
query.setPageSize(1000);
BaseResult<PageInfo<ProjectInfoListDTO>> page = projectInfoClient.queryProject(query);
if (page.getCode() != 200) {
return BaseResponse.error("调用产品中心返回错误:code:" + page.getCode() + ",message:" + page.getMessage());
}
ProjectInfo projectInfo = new ProjectInfo();
for (ProjectInfoListDTO d : page.getData().getList()) {
ProjectSynDto dto = new ProjectSynDto();
BeanUtils.copyProperties(d, dto);
projectInfo.setCode(d.getCode());
projectInfo.setDeleted((byte) 2);
int count = projectInfoMapper.selectCount(projectInfo);
if (count > 0) {
dto.setStatus("2");
} else {
projectInfo.setDeleted((byte) 0);
count = projectInfoMapper.selectCount(projectInfo);
if (count == 0) {
dto.setStatus("0");
} else {
dto.setStatus("1");
}
}
//ABS没有此项目同时项目状态为禁用 则进行隐藏
if ("0".equals(dto.getStatus())) {
if ("PROJECT_DISABLE".equals(d.getProjectStatus())) {
continue;
}
}
ProjectInfoGetDTO projectInfoGetDTO = new ProjectInfoGetDTO();
projectInfoGetDTO.setProjectCode(d.getCode());
BaseResult<ProjectDetailFullDTO> detailResult = projectInfoClient.getProject(projectInfoGetDTO);
if (detailResult != null && detailResult.getData() != null) {
ProjectDetailFullDTO detail = detailResult.getData();
//过滤掉未接入dahub的
List<ProjectProductRelFullDTO> productInfos = new ArrayList<>();
for (ProjectProductRelFullDTO productInfo : detail.getProductInfos()) {
if (0 == productInfo.getWhetherDahub()) {
continue;
}
productInfo.getProjectFieldCheckDTOS().clear();
productInfo.getProjectFileCheckInfoDTOS().clear();
productInfos.add(productInfo);
}
dto.setProjectProductRels(productInfos);
if (!CollectionUtils.isEmpty(dto.getProjectProductRels())) {
dto.setProductType(dto.getProjectProductRels().get(0).getType());
}
}
if (CollectionUtils.isEmpty(dto.getProjectProductRels()) && !"ABS_WAY".equals(dto.getType())) {
continue;
}
list.add(dto);
}
return this.getProjectSynDtoPageInfo(list, pageSize, pageNum, key);
}
这是一个查询接口,其处理逻辑可以概述为先在本系统redis中查询是否有缓存,没有则去其他系统查询,然后对返回的所有结果进行过滤后,把获得的结果放入缓存并返回。
这个方法的分支大约是2个if加一个for循环,for循环中有4个if来对其他系统返回的结果进行过滤或组装参数,循环的最后,把没有被过滤掉的数据放入数组,接口的最后是调用另一个方法来把数组放入缓存并返回给控制层。
由于代码存在这数个嵌套的控制语句,只通过阅读代码来设计全覆盖的用例是一件事倍功半的事情。而流程控制图能够帮助自己厘清所画的接口或类的结构,对于不熟悉系统的其他人员也能够通过控制图快速了解接口的决策点的分布情况,比单纯的直接阅读代码有着更高的效率。
二.控制图的基本画法
1典型的控制图,用矩形表示语句块,用菱形表示条件子句
2简化的控制图,用圆形表示条件子句,语句块则省略
摘自《微软的软件测试之道》第6章 结构测试技术,87页
这里我更加青睐抽象程度更高的圆形图的画法,后文使用了这种方法绘制了控制图。控制图绘制的要点在于区分不同的结构化程序并使用一个易于理解的画法,这里使用如下图的方法。
摘自《软件测试:一个软件工艺师的方法》第8章 路径测试,88页
控制图和步骤描述
先用表格列出每个节点表示的语句,
序号 | 描述 |
1 | 入口 |
2 | 调用preHandle接口 |
3 | if (result != null) |
4 | return result |
5 | 调用产品中心接口queryProject |
6 | if (page.getCode() != 200) |
7 | return BaseResponse.error |
8 | for循环-queryProject的返回 |
9 | if (count > 0) |
10 | setStatus("2"); |
11 | if (count == 0) |
12 | setStatus("0"); |
13 | else:setStatus("1"); |
14 | EndIf |
15 | EndIf |
16 | if ("0".equals(dto.getStatus())) |
17 | if ("PROJECT_DISABLE".equals(d.getProjectStatus())) |
18 | continue |
19 | EndIf |
20 | if (detailResult != null && detailResult.getData() != null) |
21 | for循环-detail.getProductInfos |
22 | if (0 == productInfo.getWhetherDahub()) |
23 | continue |
24 | EndIf |
25 | for循环结束 |
26 | if (!CollectionUtils.isEmpty(dto.getProjectProductRels())) |
27 | dto.setProductType(dto.getProjectProductRels().get(0).getType()) |
28 | EndIf |
29 | EndIf |
30 | if (CollectionUtils.isEmpty(dto.getProjectProductRels()) && !"ABS_WAY".equals(dto.getType())) |
31 | continue |
32 | EndIf |
33 | for循环结束 |
34 | 出口 |
绘制完毕的控制图如下
1 4和7后可以再跟一个出口,这样表述更加清晰
2 8和21是for循环,因为for循环可以看作是一个无条件的while循环,所以结构使用了和while循环一样的画法
3 控制图和步骤描述都加入了缩进,这样逻辑关系更加清晰
三.设计白盒用例
这里我采用的设计方法是块测试+条件测试,块测试确保了不同条件下不同的语句块都能被执行到,条件测试则确保了含有2个或以上布尔子表达式的条件语句的代码块都能被执行到。
条件测试的思路可以概述为
and语句
1验证每个表达式为真的场景,(只有一个场景)
2为每个表达式验证其为假,其他都为真的场景(有几个表达式就有几个场景)
or语句
1为每个表达式验证其为真,其他都为假的场景(有几个表达式就有几个场景)
2验证每个表达式都为假的场景,(只有一个场景)
来看一下具体如何获得用例
1 根据控制图,从入口1进入,到达路径3时遇到了分支if (result != null),这里选择走到4.流程结束。这是第1个用例
2 再次从入口1进入,同样在路径3,这里选择走5而不是4,到达路径6后遇到了第2个分支if (page.getCode() != 200),选择走7,流程结束。这是第2个用例
综上所述,我们的目标就是通过每个分支节点,以最小的用例数量走完所有的节点。
描述用例与分支节点的表格如下
用例1 | 3 | |||||||||||
T | 出口 | |||||||||||
用例2 | 3 | 6 | ||||||||||
F | T | 出口 | ||||||||||
用例3 | 3 | 6 | 9 | 16 | 20_1 | 30_1 | ||||||
F | F | F | F | F | F | 出口 | ||||||
用例4 | 3 | 6 | 9 | 11 | 16 | 17 | 20 | 21 | 22 | 26 | 30_2 | |
F | F | T | T | T | F | T | T | F | F | F | 出口 | |
用例5 | 3 | 6 | 9 | 11 | 16 | 17 | ||||||
F | F | T | F | T | T | continue | ||||||
用例6 | 3 | 6 | 9 | 11 | 16 | 20 | 21 | 22 | 26 | 30 | ||
F | F | T | T | F | T | T | T | T | T | continue |
在设计经过for循环的用例时,我会假设for循环的被迭代对象只有1个元素,这样,continue就意味着循环的结束。(在这个方法中,节点8的for循环完毕后就没有其他控制语句了,所以图中表示为for循环后会进入出口)
节点20和30都有2个布尔表达式的and控制语句,需要3个场景完成覆盖,这里用30_1是F表述第1个布尔表达式为假,30_2是F表述第2个布尔表达式为假,30是T表述2个布尔表达式都为真。
6个用例设计完成后,检查是否所有的分支节点都走到了,且每个分支节点是否遵循条件测试完成了场景的覆盖。这里会发现遗漏了20_2为F的场景,补上,这是用例7。
用例7 | 3 | 6 | 9 | 16 | 20_2 | 30_1 | ||||||
F | F | F | F | F | F | 出口 |
四.测试数据的获取
按照上述流程设计完用例,工作算是完成了一半(算上自动化脚本的话,可能进度只到达了1/3)
由于客观条件受限,本文无法给出具体的内容。这里只说2个我的想法,1就是尽可能地不要为了测试代码去生造不符合业务逻辑的数据,2要考虑数据的实际情况来修正用例
比如节点20和上一行的代码,熟悉java web的同行能够看出这是把调用其他系统的返回放入一个对象中,然后对这个对象中的数据有无进行逻辑判断,意思是对象中必须有接口的返回且返回的json串的Data对象不为空,才会进入之后的处理。Data对象中包含的内容就是接口返回的业务数据。在上文,我们已经使用条件测试获得了控制语句的用例,得出了20_1=F,20_2=F,20=T这3个用例。但是,Data对象存在的前提是整个返回必须存在,所以实际上20_2=F(返回不存在,Data对象存在)这个数据是不存在的,用例7要被修正为不合理的用例。(再进一步考虑,由于返回和Data对象存在依赖关系,节点20的逻辑判断应该改写为if嵌套而不是现在的and语句)
五.补遗
1 本文画图用的软件是drawio,这是一个开源的软件,可以线上画,也可以在客户端里画
2本文写到一半的时候,想到是不是已经有了能够生成控制图的软件,搜索后发现有,选用了一款叫visustin的软件试用,生成的图的抽象度较低,但仍旧足以作为自己画图的参考。该软件对本文的示例代码生成的图如下
对这个软件有兴趣的看下面的链接
https://blog.csdn.net/qq_29183811/article/details/106170648
谢谢阅读!