Expect + Where

如果业务比较复杂,对应的代码实现会有不同的分支逻辑,类似下面的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if () {
if () {
// 代码逻辑
} else {
// 代码逻辑
}
} else if () {
for () {
if () {
// 代码逻辑
} else {
// 代码逻辑
return result;
}
}
}

这样的 if else 嵌套代码因为业务的原因很难避免,如果要测试这样的代码,保证覆盖到每一个分支逻辑的话,使用传统的Junit单元测试代码写起来会很痛苦和繁琐,虽然可以使用Junit的@parametered参数化注解或者dataprovider的方式,但还是不够直观,调试起来也不方便

下面就结合具体的业务代码讲解Spock如何解决这种问题,首先看下业务代码逻辑:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.spock.example.utils;

import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;

/**
* 身份证号码工具类
* 15 位:6 位地址码+6 位出生年月日(900101 代表 1990 年 1 月 1 日出生)+3 位顺序码
* 18 位:6 位地址码+8 位出生年月日(19900101 代表 1990 年 1 月 1 日出生)+3 位顺序码+1 位校验码
* 顺序码奇数分给男性,偶数分给女性
*
* @author jinglv
* @date 2021/7/20 4:21 下午
*/
public class IDNumberUtils {
/**
* 通过身份证号码获取出生日期、性别、年龄
*
* @param certificateNo 身份证号码
* @return 返回的出生日期格式:1990-01-01 性别格式:F-女,M-男
*/
public static Map<String, String> getBirAgeSex(String certificateNo) {
int noLengthFifteen = 15;
int noLengthEighteen = 18;
String birthday = "";
String age = "";
String sex = "";

int year = Calendar.getInstance().get(Calendar.YEAR);
char[] number = certificateNo.toCharArray();
boolean flag = true;
if (number.length == noLengthFifteen) {
for (char c : number) {
if (!flag) {
return new HashMap<>(16);
}
flag = Character.isDigit(c);
}
} else if (number.length == noLengthEighteen) {
for (int x = 0; x < number.length - 1; x++) {
if (!flag) {
return new HashMap<>(16);
}
flag = Character.isDigit(number[x]);
}
}
if (flag && certificateNo.length() == noLengthFifteen) {
birthday = "19" + certificateNo.substring(6, 8) + "-"
+ certificateNo.substring(8, 10) + "-"
+ certificateNo.substring(10, 12);
sex = Integer.parseInt(certificateNo.substring(certificateNo.length() - 3)) % 2 == 0 ? "女" : "男";
age = (year - Integer.parseInt("19" + certificateNo.substring(6, 8))) + "";
} else if (flag && certificateNo.length() == noLengthEighteen) {
birthday = certificateNo.substring(6, 10) + "-"
+ certificateNo.substring(10, 12) + "-"
+ certificateNo.substring(12, 14);
sex = Integer.parseInt(certificateNo.substring(certificateNo.length() - 4,
certificateNo.length() - 1)) % 2 == 0 ? "女" : "男";
age = (year - Integer.parseInt(certificateNo.substring(6, 10))) + "";
}
Map<String, String> map = new HashMap<>(16);
map.put("birthday", birthday);
map.put("age", age);
map.put("sex", sex);
return map;
}
}

根据输入的身份证号码识别出生日期、性别、年龄等信息,逻辑不复杂,就是分支多,我们来看下Spock代码是如何测试这种情况:

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
package com.spock.example.groovy

import com.spock.example.utils.IDNumberUtils
import spock.lang.Specification
import spock.lang.Unroll

/**
* 使用Groovy Spock的测试
* @author jinglv* @date 2021/7/20 5:12 下午
*/
class IDNumberUtilsTest extends Specification {
/**
* 注意和当前时间比较,因此注意预期值
* @return
*/
@Unroll
def "身份证号:#idNo 的生日,性别,年龄是:#result"() {
expect: "when + then 组合"
IDNumberUtils.getBirAgeSex(idNo) == result

where: "表格方式测试不同的分支逻辑"
idNo || result
"310168199809187333" || ["birthday": "1998-09-18", "sex": "男", "age": "23"]
"320168200212084268" || ["birthday": "2002-12-08", "sex": "女", "age": "19"]
"330168199301214267" || ["birthday": "1993-01-21", "sex": "女", "age": "28"]
"411281870628201" || ["birthday": "1987-06-28", "sex": "男", "age": "34"]
"427281730307862" || ["birthday": "1973-03-07", "sex": "女", "age": "48"]
"479281691111377" || ["birthday": "1969-11-11", "sex": "男", "age": "52"]
}
}

在测试方法体的第一行使用了expect标签,它的作用是when + then标签的组合,即 “什么时候做什么 + 然后验证什么结果” 组合起来

即当调用IDNumberUtils.getBirAgeSex(idNo) 方法时,验证结果是result,result如何验证对应的就是where里的result一列的数据,当输入参数idNo是”310168199809187333”时,返回结果是: [“birthday”: “1998-09-18”, “sex”: “男”, “age”: “23”]

expect可以单独使用,可以不需要where,只是在这个场景需要

@Unroll注解表示展开where标签下面的每一行测试,作为单独的case跑,再加上方法体”身份证号:#idNo 的生日,性别,年龄是:#result”,使用了Groovy的字面量特性,动态替换字符串变量,这样每次跑的单测结果展示也很容易区分,方便理解,如下:

image-20210721163927562

每个测试结果对应where标签里的一行

在 intellij idea 里可以 run with coverage 的运行方式查看单测覆盖率情况:

image-20210722110606774

为了能明确的看到覆盖率的显示,将身份证号码为15位的case注释,再次执行

image-20210722110822903

上图圈出绿色的柱子表示单测已覆盖的代码,红色柱子是单测还没有覆盖到的代码,也很明确看到覆盖率,类覆盖100%,方法覆盖100%,行覆盖是72%,如果进一步提高覆盖率,只需要在where标签中添加测试用例即可

Spock与Jacoco

Jacoco是统计单元测试覆盖率的一种工具,当然Spock也自带了覆盖率统计的功能,这里使用第三方Jacoco的原因主要是国内公司使用的比较多一些,包括我们公司现在使用的也是Jacoco,所以为了兼容就以Jacoco来查看单测覆盖率

在pom文件里引用jacoco的插件: jacoco-maven-plugin, 然后执行mvn test 命令,成功后会在target目录下生成单元测试覆盖率的报告:

pom中引入依赖

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
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- jacoco统计单测覆盖率 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.1</version>
<configuration>
<includes>
<include>**/*</include>
</includes>
<destFile>${project.build.directory}/coverage-reports/jacoco-unit.exec</destFile>
<dataFile>${project.build.directory}/coverage-reports/jacoco-unit.exec</dataFile>
</configuration>
<executions>
<execution>
<id>jacoco-initialize</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-site</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

执行命令:mvn clean test

注意:Groovy代码必须放在test包下,新建的groovy包,并且该包为Test Root,如果放在其他的包下,则在命令行执行时,则不会执行

image-20210722111130340

执行结果

image-20210722111259012

使用浏览器打开index.html,就能看到所有的单测覆盖率统计指标:

image-20210722111337361

点击包名找到我们刚才测试的IDNumberUtils类,打开后可以看到具体的覆盖情况:

image-20210722111532683

  • 绿色背景表示完全覆盖
  • 黄色是部分覆盖
  • 红色没有覆盖到

比如第36行黄色的else if()判断,在if代码段里的for循环中if条件判断(38行)也为黄色,则是说明36行未覆盖全是因为38行待判断条件未覆盖,我们的测试用例中没有flag为false,以及certificateNo.length()!=18的场景,所以只能算覆盖了一半(2/4)。

我们在做单元测试时,可以通过这种方式判断有哪些判断条件未覆盖到,如果公司设置的分支覆盖率要求大于50%时,我们则通过这种方式增加测试用例。

学习大佬文章:https://javakk.com/281.html