一. Expect + Where
如果业务比较复杂,对应的代码实现会有不同的分支逻辑,类似下面的伪代码:
if () {
if () {
// 代码逻辑
} else {
// 代码逻辑
}
} else if () {
for () {
if () {
// 代码逻辑
} else {
// 代码逻辑
return result;
}
}
}
这样的 if else
嵌套代码因为业务的原因很难避免,如果要测试这样的代码,保证覆盖到每一个分支逻辑的话,使用传统的Junit单元测试代码写起来会很痛苦和繁琐,虽然可以使用Junit的@parametered
参数化注解或者dataprovider的方式,但还是不够直观,调试起来也不方便
下面就结合具体业务代码讲解Spock如何解决这种问题,还是先看下业务代码逻辑:
/**
* 身份证号码工具类<p>
* 15位:6位地址码+6位出生年月日(900101代表1990年1月1日出生)+3位顺序码
* 18位:6位地址码+8位出生年月日(19900101代表1990年1月1日出生)+3位顺序码+1位校验码
* 顺序码奇数分给男性,偶数分给女性。
* @author 公众号:Java老K
* 个人博客:www.javakk.com
*/
public class IDNumberUtils {
/**
* 通过身份证号码获取出生日期、性别、年龄
* @param certificateNo
* @return 返回的出生日期格式:1990-01-01 性别格式:F-女,M-男
*/
public static Map<String, String> getBirAgeSex(String certificateNo) {
String birthday = "";
String age = "";
String sex = "";
int year = Calendar.getInstance().get(Calendar.YEAR);
char[] number = certificateNo.toCharArray();
boolean flag = true;
if (number.length == 15) {
for (int x = 0; x < number.length; x++) {
if (!flag) return new HashMap<>();
flag = Character.isDigit(number[x]);
}
} else if (number.length == 18) {
for (int x = 0; x < number.length - 1; x++) {
if (!flag) return new HashMap<>();
flag = Character.isDigit(number[x]);
}
}
if (flag && certificateNo.length() == 15) {
birthday = "19" + certificateNo.substring(6, 8) + "-"
+ certificateNo.substring(8, 10) + "-"
+ certificateNo.substring(10, 12);
sex = Integer.parseInt(certificateNo.substring(certificateNo.length() - 3,
certificateNo.length())) % 2 == 0 ? "女" : "男";
age = (year - Integer.parseInt("19" + certificateNo.substring(6, 8))) + "";
} else if (flag && certificateNo.length() == 18) {
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<>();
map.put("birthday", birthday);
map.put("age", age);
map.put("sex", sex);
return map;
}
}
根据输入的身份证号码识别出生日期、性别、年龄等信息,逻辑不复杂,就是分支多,我们来看下Spock代码是如何测试这种情况:
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": "22"]
"320168200212084268" || ["birthday": "2002-12-08", "sex": "女", "age": "18"]
"330168199301214267" || ["birthday": "1993-01-21", "sex": "女", "age": "27"]
"411281870628201" || ["birthday": "1987-06-28", "sex": "男", "age": "33"]
"427281730307862" || ["birthday": "1973-03-07", "sex": "女", "age": "47"]
"479281691111377" || ["birthday": "1969-11-11", "sex": "男", "age": "51"]
}
}
在测试方法体的第一行使用了expect
标签,它的作用是when
+ then
标签的组合,即 "什么时候做什么 + 然后验证什么结果" 组合起来
即当调用IDNumberUtils.getBirAgeSex(idNo)
方法时,验证结果是result,result如何验证对应的就是where里的result一列的数据,当输入参数idNo是"310168199809187333"时,返回结果是: ["birthday": "1998-09-18", "sex": "男", "age": "22"]
expect
可以单独使用,可以不需要where
,只是在这个场景需要
@Unroll
注解表示展开where标签下面的每一行测试,作为单独的case跑,再加上方法体"身份证号:#idNo 的生日,性别,年龄是:#result",使用了groovy的字面量特性,动态替换字符串变量,这样每次跑的单测结果展示也很容易区分,方便理解,如下:
每个测试结果对应where标签里的一行
另外在intellij idea里可以run with coverage的运行方式查看单测覆盖率情况:
左边圈出的绿色柱子表示单测已覆盖的代码,红色柱子是单测还没有覆盖到的分支,如果需要进一步提高覆盖率,只需在where表格中再添加一行测试条件即可
(完整的源码在公众号: java老k 里回复spock获取)
二. Jacoco
Jacoco是统计单元测试覆盖率的一种工具,当然Spock也自带了覆盖率统计的功能,这里使用第三方Jacoco的原因主要是国内公司使用的比较多一些,包括我们公司现在使用的也是Jacoco,所以为了兼容就以Jacoco来查看单测覆盖率
当然你也可以使用Spock自带的单测覆盖率工具,在后面的文章里会介绍具体如何配置,本篇主要说下如何通过Jacoco确认分支是否完全覆盖到
在pom文件里引用jacoco的插件: jacoco-maven-plugin
, 然后执行mvn test
命令,成功后会在target目录下生成单元测试覆盖率的报告:
(具体生成路径可以设置)
使用浏览器打开index.html,就能看到所有的单测覆盖率统计指标:
点击包名找到我们刚才测试的IDNumberUtils类,打开后可以看到具体的覆盖情况:
绿色背景表示完全覆盖,黄色是部分覆盖,红色没有覆盖到
比如第45行黄色背景的else if()
判断,提示有4分之2的分支缺失,虽然它下面的代码也被覆盖了(显示为绿色),但是因为我们的单测代码没有测试flag为false,以及certificateNo.length()!=18
的场景,所以只能算覆盖了一半(2/4)
讲这个的原因是因为如果公司设置的分支覆盖率要求大于50%,那么你就要在单元测试代码里额外增加这种情况的测试,即使业务代码里没有这样例外情况的处理
这种情况跟具体使用哪种单测框架没关系,因为这只是分支覆盖率统计的规则,只不过使用Spock的话,解决起来会更简单,只需在where下增加一行针对的测试数据即可
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。