cover_image

Jacoco + 覆盖率平台在 Mercury 白盒测试中的实践

施勤 唯品会质量工程
2017年09月04日 12:23

白盒测试概述

白盒测试是基于对系统内部一定了解之上的测试技术。测试人员需要拥有源代码相关权限与对系统架构的了解。测试人员需要进行源代码分析,在此基础上再根据源代码设计测试用例,并最终达到一定的代码覆盖率。

白盒测试关注点包括:安全漏洞、不可用或者不完整的路径、与相关说明文档的一致性、输出结果是否预期、所有的条件循环语句等。

广义上来讲,白盒测试包括如下两种方法:

  • 静态白盒测试

    浏览代码,凭借经验,找出代码中的错误或者代码中不符合书写规范的地方,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:

  1. 不依赖于GUI就可以进行白盒测试

  2. 可以帮助覆盖全路径

  3. 测试人员能够对代码提出改进建议

  4. 因为测试人员了解代码内部结构,可以使用更高效的测试数据

  5. 白盒测试能够更好地促进优化代码

Cons:

  1. 需要对代码内部结构有深入了解的高技术人员来进行测试,提高了成本

  2. 如果代码频繁变动就需要更新测试脚本

  3. 如果应用测试量很庞大,完全测试是不可能的

  4. 不可能测试系统的每个路径或者条件(而路径/条件可能有缺陷)

  5. 白盒测试相对代价大

  6. 分析每行每条路径几乎是不可能的

  7. 需要使用不同的输入条件来测试每条路径或者条件,所以测试人员需要准备大量测试数据,而这个过程可能很耗时。

Appendix : Jacoco Maven plugin 的配置

在 pom.xml 中按下面例子配置,加入对 Jacoco Maven plugin 的依赖,即可在执行 mvn test 命令后生成覆盖率报告(位于 target/site/jacoco 目录下)。

Note:

  1. 如果将 <phase>test</phase> 改成 <phase>prepare-package</phase> 在运行 mvn test 是不会出报告的,需要运行 mvn package 才能看得到。具体原因,大家可了解下 maven project 的生命周期。

  2. 有关 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>


长按以下二维码,关注本公众号,更多精彩原创等着你!

图片

继续滑动看下一个
唯品会质量工程
向上滑动看下一个