白盒测试概述
白盒测试关注点包括:安全漏洞、不可用或者不完整的路径、与相关说明文档的一致性、输出结果是否预期、所有的条件循环语句等。
广义上来讲,白盒测试包括如下两种方法:
●静态白盒测试
浏览代码,凭借经验,找出代码中的错误或者代码中不符合书写规范的地方,CodeReview 是一种不错的方式。
比如下面的代码,表示 Mercury 每30秒进行 metrix 落盘记录。
this.registry = MetricsRegistryBuilder.create().setPollingInterval(30000L).setLoggerReporterName(loggerName).build();
但相关处理的代码如下:
... result.setStartTime(ts); result.setEndTime(ts + 1000); ... |
这里的startTime与endTime的差值一直为1000ms,所以导致无论setPollingInterval传递参数是多少,落盘数据的时间始终为1000ms。
●动态白盒测试
通过执行/调试过程,遍历代码各分支来进行测试;
白盒测试一般包括如下两个步骤:
●Step 1:理解源代码
熟悉系统所用的编程语言,有时候同样需要有系统安全性的相关知识。
●Step 2: 创建与执行测试用例
根据白盒测试技术(语句、条件、判断等)编写并执行测试用例。下面简单讲述一下白盒测试技术。
白盒测试技术
1. 语句覆盖(Statement Coverage)
被测代码中每个可执行语句是否被执行到。下图是 Mercury 白盒测试中的覆盖率截图。其中绿/黄色行表示被执行到的语句,红色行表示未被执行到(黄色行的含义见后面的 Jacoco 中的分支和条件覆盖率)。
2. 分支覆盖(Branch Coverage)
代码中每个分支是否都被覆盖。对于 if 语句,true 和 false 分支都走到了,才能说全部分支都覆盖了;对于 switch-case 需要每个 case 和 default 都需要走到。
3. 条件覆盖(Condition Coverage)
每个判断中每个条件的可能取值至少满足一次
比如,对于如下条件语句
if ( a > 5 && b < 3 ) { ... } |
有以下4种场景用例才能覆盖全:
a > 5 && b >= 3 【真&&假,结果:false】
a <= 5 && b < 3 【假&&真,结果:false】
a <= 5 && b >= 3 【假&&假,结果:false】
a > 5 && b < 3 【真&&真,结果:true】
4. 路径测试
路径测试可以保证一个模块中的所有独立路径至少被使用一次。具体操作上,可以先画出流程图、计算圈复杂度、再根据独立路径来设计测试用例。
以下为 Mercury 中一段校验 HTTP 参数的代码:
if (null == component.getCode() || component.getCode().trim().isEmpty()) { response.setCode(BaseResponse.PARAM_ERROR); response.setMsg("参数(code)缺失");} else if (component.getCode().length() > 64) { response.setCode(BaseResponse.PARAM_ERROR); response.setMsg("Code长度不得超过64个字符");} else if (component.getName() != null && component.getName().length() > 64) { response.setCode(BaseResponse.PARAM_ERROR); response.setMsg("Name长度不得超过64个字符");} else if (component.getDescription() != null && component.getDescription().length() > 255) { response.setCode(BaseResponse.PARAM_ERROR); response.setMsg("描述长度不得超过255个字符");} else { MetricComponentType exist = metricConfigService.getComponent(component.getCode()); if (exist != null) { response.setCode(BaseResponse.PARAM_ERROR); response.setMsg("相同配置(" + component.getCode() + ")已存在"); } else { response.setData(metricConfigService.createComponent(component)); response.setCode(BaseResponse.SUCCESS); response.setMsg("ok"); } } |
代码中有 5 个判断,因而 判定节点个数为5,由于 圈复杂度(独立路径个数)= 判定节点 + 1,因而以上代码圈复杂度为:5 + 1 = 6,我们至少需要 6 个测试用例来遍历所有独立路径。
圈复杂度(cyclomatic complexity):
用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护。根据经验,程序的可能错误和高的圈复杂度有着很大关系。代码复杂度增加会导致几乎不可能画出流程图并且计算出圈复杂度。
Jacoco 中的分支和条件覆盖率
很多统计覆盖率的工具中并没有区分条件覆盖和分支覆盖。举个例子,下面的 Example类中的 if 语句,看上去测试类 BranchCoverageTest 应该覆盖了两个分支。但 Jacoco 的报告中却显示 2 of 6 branches missed.。
package com.test; public class Example { public boolean branchFunc(int x, int y, int z) { if (x > 0 || y > 0 || z > 0) { return true; } else { return false; } } } import com.test.Example; import org.junit.Test; public class BranchCoverageTest { @Test public void testBranchCoverage(){ Example bct = new Example(); bct.branchFunc(0, 0, 0); bct.branchFunc(1, 0, 0); } } |
其实在 Jacoco 里,每个 Yes、No 都是 1 个 Branch。如下图所示,B1、B2、B3、B4、B5、B6 都是一个 branch。上述用例其实只覆盖了 B1、B4、B5、B6 而 B2、B3 并没有覆盖,所以最终的 branch coverage 为 66%。
在 test case 里增加 (0, 1, 0), (0, 0, 1) 最终的测试结果中 branch coverage 为 100%。
常用的覆盖率指标
行覆盖率
度量被测程序的每行代码是否被执行,判断标准行中是否至少有一个指令被执行。
类覆盖率
度量计算class类文件是否被执行。
分支覆盖率
度量if和switch语句的分支覆盖情况,计算一个方法里面的总分支数,确定执行和不执行的 分支数量。
方法覆盖率
度量被测程序的方法执行情况,是否执行取决于方法中是否有至少一个指令被执行。
指令覆盖
计数单元是单个java二进制代码指令,指令覆盖率提供了代码是否被执行的信息,度量完全 独立源码格式。
圈复杂度
在(线性)组合中,计算在一个方法里面所有可能路径的最小数目,缺失的复杂度同样表示测 试案例没有完全覆盖到这个模块。
Example: 利用覆盖率平台提升 Mercury 覆盖率
覆盖率平台是 VIP 自主研发的内部用于查看用例覆盖程度的工具。怎么使用测试覆盖率这里不进行说明,感兴趣的读者可查看历史消息 浅谈唯品会测试覆盖率平台。
工作中,我们可以使用测试覆盖率平台分析代码来提高测试覆盖率,通过下面浅显易懂的例子来说明。
在一次 Mercury 的功能变动后,在覆盖率平台上看到一块处理代码的覆盖率急剧下降(如下图所示),只剩下 else 语句被覆盖:
可以看到独立路径只覆盖了1个,分支覆盖率也为50%:
跟之前说明过的一样,红色为未覆盖的路径,分析得到我们需要提高独立路径覆盖率,这里需要额外7个测试用例:
●使用非管理员账号发送请求
●请求参数中不填写开始或者结束时间
●请求参数中开始时间大于结束时间
●请求参数中不包含name
●请求参数中name长度256
●请求参数中不含有item
●请求参数中item长度256
●另外要实现分支覆盖,针对
else if (model.getStart() == null || model.getEnd() == null)
我们使用这8个测试用例:
使用非管理员账号发送请求
请求参数中不填写开始时间,有结束时间
请求参数中有开始时间,没有结束时间
请求参数中开始时间大于结束时间
请求参数中不包含name
请求参数中name长度256
请求参数中不含有item
请求参数中item长度256
最后覆盖率结果,独立路径覆盖率与分支覆盖率都达到了100%:
Note: 该例中因 else 和最后的 “}“ 使得行覆盖率只有 90%,小于分支覆盖率。 由于分支覆盖中包含各种条件,比如之前说的 if (x >0 || y > 0 || z >0 ) 例子,两个TC就能使得行覆盖率达到 100%, 但要让分支覆盖达到 100% 却需要 4个 TC,因而 分支覆盖率相对行覆盖率来说更严格。
白盒测试的利弊
每种测试方法都有其利弊,白盒也不例外。这里列举一些白盒测试的利弊,可以在权衡之后选择是否进行白盒测试:
Pros:
不依赖于GUI就可以进行白盒测试
可以帮助覆盖全路径
测试人员能够对代码提出改进建议
因为测试人员了解代码内部结构,可以使用更高效的测试数据
白盒测试能够更好地促进优化代码
Cons:
需要对代码内部结构有深入了解的高技术人员来进行测试,提高了成本
如果代码频繁变动就需要更新测试脚本
如果应用测试量很庞大,完全测试是不可能的
不可能测试系统的每个路径或者条件(而路径/条件可能有缺陷)
白盒测试相对代价大
分析每行每条路径几乎是不可能的
需要使用不同的输入条件来测试每条路径或者条件,所以测试人员需要准备大量测试数据,而这个过程可能很耗时。
Appendix : Jacoco Maven plugin 的配置
在 pom.xml 中按下面例子配置,加入对 Jacoco Maven plugin 的依赖,即可在执行 mvn test 命令后生成覆盖率报告(位于 target/site/jacoco 目录下)。
Note:
如果将 <phase>test</phase> 改成 <phase>prepare-package</phase> 在运行 mvn test 是不会出报告的,需要运行 mvn package 才能看得到。具体原因,大家可了解下 maven project 的生命周期。
有关 Jacoco Maven plugin 定义的 goal 及相关参数配置,可去 http://www.eclemma.org/jacoco/trunk/doc/maven.html 查看。
<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.7.9</version> <configuration> <!-- 指定需要统计覆盖率的类,jacoco 有坑见这里在尾部加个 * 作为 workaround 可见 https://github.com/jacoco/jacoco/issues/34 --> <includes> <include>com/test/Example*</include> </includes> </configuration> <executions> <execution> <!-- 在maven的initialize阶段,将Jacoco的runtime agent作为VM的一个参数 传给被测程序,用于监控JVM中的调用。--> <id>default-prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> <configuration> <destFile>${project.build.directory}/coverage-reports/jacoco.exec</destFile> </configuration> </execution> <execution> <id>default-report</id> <phase>test</phase> <goals> <goal>report</goal> </goals> <configuration> <dataFile>${project.build.directory}/coverage-reports/jacoco.exec</dataFile> <!-- 过滤 report 中需要展示/不展示的类 --> <!--<includes>com/test/*</includes>--> <!--<excludes>annot/*</excludes>--> <outputDirectory>${project.reporting.outputDirectory}/jacoco</outputDirectory> </configuration> </execution> <execution> <id>default-check</id> <goals> <goal>check</goal> </goals> </execution> </executions> </plugin> |