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;
public class IDNumberUtils {
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
class IDNumberUtilsTest extends Specification {
@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的字面量特性,动态替换字符串变量,这样每次跑的单测结果展示也很容易区分,方便理解,如下:

每个测试结果对应where标签里的一行
在 intellij idea 里可以 run with coverage 的运行方式查看单测覆盖率情况:

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

上图圈出绿色的柱子表示单测已覆盖的代码,红色柱子是单测还没有覆盖到的代码,也很明确看到覆盖率,类覆盖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> <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,如果放在其他的包下,则在命令行执行时,则不会执行

执行结果

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

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

- 绿色背景表示完全覆盖
- 黄色是部分覆盖
- 红色没有覆盖到
比如第36行黄色的else if()判断,在if代码段里的for循环中if条件判断(38行)也为黄色,则是说明36行未覆盖全是因为38行待判断条件未覆盖,我们的测试用例中没有flag为false,以及certificateNo.length()!=18的场景,所以只能算覆盖了一半(2/4)。
我们在做单元测试时,可以通过这种方式判断有哪些判断条件未覆盖到,如果公司设置的分支覆盖率要求大于50%时,我们则通过这种方式增加测试用例。
学习大佬文章:https://javakk.com/281.html