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

image

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

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

image

image

左边圈出的绿色柱子表示单测已覆盖的代码,红色柱子是单测还没有覆盖到的分支,如果需要进一步提高覆盖率,只需在where表格中再添加一行测试条件即可

(完整的源码在公众号: java老k 里回复spock获取)

二. Jacoco

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

当然你也可以使用Spock自带的单测覆盖率工具,在后面的文章里会介绍具体如何配置,本篇主要说下如何通过Jacoco确认分支是否完全覆盖到

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

image

(具体生成路径可以设置)

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

image

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

image

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

比如第45行黄色背景的else if()判断,提示有4分之2的分支缺失,虽然它下面的代码也被覆盖了(显示为绿色),但是因为我们的单测代码没有测试flag为false,以及certificateNo.length()!=18的场景,所以只能算覆盖了一半(2/4)

讲这个的原因是因为如果公司设置的分支覆盖率要求大于50%,那么你就要在单元测试代码里额外增加这种情况的测试,即使业务代码里没有这样例外情况的处理

这种情况跟具体使用哪种单测框架没关系,因为这只是分支覆盖率统计的规则,只不过使用Spock的话,解决起来会更简单,只需在where下增加一行针对的测试数据即可

文章来源:http://javakk.com/281.html


Java老K
36 声望12 粉丝

十年java老兵,现就职上海某一线互联网大厂,专注java技术,包括多线程并发,RXjava,JVM,Spring,Springboot,DDD,分布式中间件Dubbo,kafka,redis,微服务等,不定期分享面试题和业界最新动态以及人生感悟。