4年前 (2020-08-09)  Spock系列 |   2 条评论  2827 
文章评分 2 次,平均分 5.0
[收起] 文章目录

这是Spock系列的第四篇文章,在第二篇讲单元测试开发成本和效率问题时,提到了如何测试复杂的if else场景,分别使用Junit和Spock的实现,以及Spock的优势在哪里,这一篇会详细讲解Spock代码的语法

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

Spock代码讲解- if esle 多分支场景测试

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

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

Spock代码讲解- if esle 多分支场景测试

Spock代码讲解- if esle 多分支场景测试

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

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

二. Jacoco

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

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

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

Spock代码讲解- if esle 多分支场景测试

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

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

Spock代码讲解- if esle 多分支场景测试

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

Spock代码讲解- if esle 多分支场景测试

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

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

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

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

下一篇文章讲下如何测试代码中抛异常的情况

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/281.html

关于

发表评论

表情 格式
  1. 老哥,你好,Calendar.getInstance().get(Calendar.YEAR) 作为外部依赖,应该需要mock。不然这个单元测试,今年跑可以,明年就失败。所以我在尝试mock时,发现了一个问题。


    @RunWith(PowerMockRunner.class)
    @PrepareForTest(Calendar.class)
    public class IDNumberUtilsJavaTest {
    @Test
    public void testCalendar(){
    PowerMockito.mockStatic(Calendar.class);
    PowerMockito.when(Calendar.getInstance().get(1)).thenReturn(2020);
    int i = Calendar.getInstance().get(1);
    System.out.println(i);
    assertEquals(2020, i);
    }
    }

    单纯powermockit没有问题。但结合Spock时就会报错
    org.mockito.exceptions.misusing.MissingMethodInvocationException:
    when() requires an argument which has to be ‘a method call on a mock’.
    For example:
    when(mock.getArticles()).thenReturn(articles);


    @RunWith(PowerMockRunner.class)
    @PowerMockRunnerDelegate(Sputnik.class)
    @PrepareForTest(Calendar.class)
    class IDNumberUtilsTest extends Specification {
    def"测试"(){
    given:
    PowerMockito.mockStatic(java.util.Calendar.class)
    PowerMockito.when(java.util.Calendar.getInstance()).get(1).thenReturn(2020)

    when:
    def year = java.util.Calendar.getInstance().get(1)
    println(year)

    then:
    year == 2021
    }
    }

    不知道是不是powermockito和spock是不是不够兼容。这个问题比较钻牛角尖哈。但确实困扰我很久

    有1024个人 评论达人 LV.1 4年前 (2021-04-25) [2] [0]
    • @有1024个人迟复为歉,谢谢你的反馈,确实存在这个问题,原因是Groovy和powermock在实现原理上的差异导致的不兼容问题,可以使用stub代替when方法,或使用@compileStatic注解,详细用法可以参考这篇文章:https://javakk.com/2038.html

      sofia 博 主 3年前 (2021-06-19) [1] [0]

登录

忘记密码 ?

切换登录

注册