关于java:Spock单元测试框架实战指南一Spock是什么它和JUnit有什么区别

5次阅读

共计 7362 个字符,预计需要花费 19 分钟才能阅读完成。

这是 Spock 系列的第一篇文章,整个专辑会介绍 Spock 的用处,为什么应用 Spock?它能给咱们带来什么益处?它和 JUnit、JMock、Mockito 有什么区别?咱们平时写单元测试代码的常见问题和痛点,Spock 又是如何解决的,Spock 的代码怎么编写以及 Spock 的劣势和毛病等内容,让大家对 Spock 有个主观的理解。

Spock 是什么?

斯波克是国外一款优良的测试框架,基于 BDD 思维,功能强大,可能让咱们的测试 代码标准 化,构造 档次清晰 ,联合groovy 动静语言 的特点以及本身提供的各种标签让编写测试代码更加 高效 简洁,提供一种通用、简略、结构化的描述语言

援用官网的介绍如下(http://spockframework.org)


“Spock 是一个 Java 和 Groovy 应用程序的测试和标准框架。

它之所以能在人群中怀才不遇,是因为它柔美而富裕表现力的标准语言。

斯波克的灵感来自 JUnit、RSpec、jMock、Mockito、Groovy、Scala、Vulcans”

简略说 Spock 的特点如下:

  • 让咱们的测试代码更标准,内置多种标签来标准单测代码的语义,从而让咱们的测试代码构造清晰,更具可读性,升高前期保护难度
  • 提供多种标签,比方: wherewiththrown… 帮忙咱们应答简单的测试场景
  • 再加上应用 groovy 这种动静语言来编写测试代码,能够让咱们编写的测试代码更简洁,适宜麻利开发,进步编写单测代码的效率
  • 听从 BDD 行为驱动开发模式,不单是为了测试覆盖率而测试,有助于晋升代码品质
  • IDE 兼容性好,自带 mock 性能

为什么应用 Spock?Spock 和 JUnit、JMock、Mockito 的区别在哪里?

收到现有的单测框架比方 junit、jmock、mockito 都是绝对独立的工具,只是针对不同的业务场景提供特定的解决方案。

Junit 单纯用于测试,不提供 mock 性能

微服务曾经是互联网公司的支流技术架构,大部分的零碎都是分布式,服务与服务之间个别通过接口的形式交互,甚至服务外部也划分成多个 module,很多业务性能须要依赖底层接口返回的数据能力持续剩下的流程,或者从数据库 /Redis 等存储设备上获取,或是从配置核心的某个配置获取。

这样就导致如果咱们想要测试代码逻辑是否正确,就必须把这些依赖项 (接口、Redis、DB、配置核心 …) 给 mock 掉。

如果接口不稳固或有问题则会影响咱们代码的失常测试,所以咱们要把调用接口的中央给 模仿 掉,让它返回指定的后果(提前准备好的数据),这样能力往下验证咱们本人的代码是否正确,合乎预期逻辑和后果。

JMock 或 Mockito 尽管提供了 mock 性能,能够把接口等依赖屏蔽掉,但不提供对动态类静态方法的 mock,PowerMock 或 Jmockit 尽管提供动态类和办法的 mock,但它们之间须要整合 (junit+mockito+powermock),语法繁琐,而且这些工具并没有通知你“ 单元测试代码到底应该怎么写?

工具多了也会导致不同的人写出的单元测试代码形形色色,格调迥异。。。

Spock 通过提供标准形容,定义多种标签 (givenwhenthenwhere 等)去形容代码“应该做什么”,输出条件是什么,输入是否合乎预期,从语义层面标准代码的编写。

Spock 自带 Mock 性能,应用简略不便(也反对扩大其余 mock 框架,比方 power mock),再加上 groovy 动静语言的弱小语法,能写出简洁高效的测试代码,同时更不便直观的验证业务代码行为流转,加强咱们对代码执行逻辑的可控性。

背景和初衷

网上对于 Spock 的材料比较简单,包含官网的 demo,无奈解决咱们我的项目中的简单业务场景,须要找到一套适宜本人我的项目的成熟解决方案,所以感觉有必要把咱们我的项目中应用 Spock 的教训分享进去, 帮忙大家晋升单测开发的效率和验证代码品质。

在熟练掌握 Spock 后咱们项目组整体的单测开发效率晋升了 50% 以上,代码可读性和维护性都失去了改善和晋升。

适宜人群

写 Java 单元测试的开发小伙伴和测试同学,所有的演示代码运行在 IntelliJ IDEA 中,spring-boot 我的项目,基于 Spock 1.3-groovy-2.5 版本

Spock 如何解决传统单元测试开发中的痛点

这篇次要讲下咱们平时写单元测试过程中遇到的几种常见问题,别离应用 JUnit 和 Spock 如何解决,通过比照的形式给大家一个整体意识。

一. 单元测试代码开发的老本和效率

简单场景的业务代码,在分支 (if/else) 很多的状况下,编写单测代码的老本会相应减少,失常的业务代码或者只有几十行,但为了测试这个性能,要笼罩大部分的分支场景,写的测试代码可能远远不止几十行

举个咱们生产环境前不久产生的一起事变:有个性能上线 1 年多始终都失常,没有出过问题,但最近有个新的调用方申请的数据不一样,走到了代码中一个不罕用的分支逻辑,导致了 bug,间接抛出异样阻断了主流程,好在调用方申请量不大。。。

预计当初写这段代码的同学也认为很小几率会走到这个分支,尽管过后也写了单元测试代码,但分支较多,刚好漏掉了这个分支逻辑的测试,给日后上线留下了隐患

这也是咱们平时写单元测试最常遇到的问题:要达到分支覆盖率高要求的状况下,if/else有不同的后果,传统的单测写法可能要屡次调用,能力笼罩全副的分支场景,一个是写单测麻烦,同时也会减少单测代码的冗余度

尽管能够应用 junit 的 @parametered 参数化注解或者 dataprovider 的形式,但还是不够不便直观,而且如果其中一次分支测试 case 出错的状况下,报错信息也不够详尽。

比方上面的示例演示代码,依据输出的身份证号码辨认出生日期、性别、年龄等信息,这个办法的特点就是有很多 if...else... 的分支嵌套逻辑

/**
 * 身份证号码工具类 <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 提供了 where 标签,让咱们能够通过表格的形式不便测试多种分支

上面的比照图是针对 ” 依据身份证号码获取出生日期、性别、年龄 ” 办法实现的单元测试,右边是咱们罕用的 Junit 的写法,左边是 Spock 的写法,红框圈进去的是一样的性能在 Junit 和 Spock 上的代码实现 (两边执行的单测后果一样,点击放大查看差别)

比照后果:

左边一栏应用 Spock 写的单测代码上语法简洁,表格形式测试笼罩多分支场景也更直观,晋升开发效率,更适宜麻利开发

(对于 Spock 代码的具体语法会在后续文章解说)

二. 单元测试代码的可读性和前期保护

微服务架构下,很多场景须要依赖其余接口返回的后果能力验证本人代码的逻辑,这样就须要应用 mock 工具,但 JMock 或 Mockito 的语法比拟繁琐,再加上单测代码不像业务代码那么直观,不能齐全依照业务流程的思路写单测,以及开发同学对单测代码可读性的不器重,最终导致测试代码难于浏览,保护起来更是难上加难

可能本人写完的测试,过几天再看就云里雾里了(当然增加正文会好很多),再比方改了原来的代码逻辑导致单测执行失败,或者新增了分支逻辑,单测没有笼罩到,随着后续版本的迭代,会导致单测代码越来越臃肿和难以保护

Spock 提供多种语义标签,如: given、when、then、expect、where、with、and 等,从行为上标准单测代码,每一种标签对应一种语义,让咱们的单测代码构造具备层次感,功能模块划分清晰,便于前期保护

Spock 自带 mock 性能,应用上简略不便(Spock 也反对扩大第三方 mock 框架,比方 power mock)保障代码更加标准,构造模块化,边界范畴清晰,可读性强,便于扩大和保护,用自然语言形容测试步骤,让非技术人员也能看懂测试代码

比方上面的业务代码:

调用用户接口或者从数据库获取用户信息,而后做一些转换和判断逻辑(这里的业务代码只是列举常见的业务场景,不便演示)

/**
 * 用户服务
 * @author 公众号:Java 老 K
 * 集体博客:www.javakk.com
 */
@Service
public class UserService {

    @Autowired
    UserDao userDao;

    @Autowired
    MoneyDAO moneyDAO;

    public UserVO getUserById(int uid){List<UserDTO> users = userDao.getUserInfo();
        UserDTO userDTO = users.stream().filter(u -> u.getId() == uid).findFirst().orElse(null);
        UserVO userVO = new UserVO();
        if(null == userDTO){return userVO;}
        userVO.setId(userDTO.getId());
        userVO.setName(userDTO.getName());
        userVO.setSex(userDTO.getSex());
        userVO.setAge(userDTO.getAge());
        // 显示邮编
        if("上海".equals(userDTO.getProvince())){userVO.setAbbreviation("沪");
            userVO.setPostCode(200000);
        }
        if("北京".equals(userDTO.getProvince())){userVO.setAbbreviation("京");
            userVO.setPostCode(100000);
        }
        // 手机号解决
        if(null != userDTO.getTelephone() && !"".equals(userDTO.getTelephone())){userVO.setTelephone(userDTO.getTelephone().substring(0,3)+"****"+userDTO.getTelephone().substring(7));
        }

        return userVO;
    }
}

上面的比照图是别离应用 Junit 和 Spock 实现的单元测试,右边是 Junit 的写法,左边是 Spock,红框圈进去的是一样的性能在 Junit 和 Spock 上的实现 (两边执行的单测后果一样,点击放大查看差别)

比照后果:

右边的 junit 单测代码冗余,短少构造档次,可读性差,随着后续迭代势必会导致代码的沉积,前期保护老本会越来越高。

左边的单测代码 spock 会强制要求应用 givenwhenthen 这样的语义标签 (至多一个),否则编译不通过,这样保障代码更加 标准 ,构造 模块化 ,边界范畴清晰, 可读性强 ,便于扩大和保护,用自然语言形容测试步骤,让非技术人员也能看懂测试代码(given 示意输出条件,when触发动作,then验证输入后果)

Spock 自带的 mock 语法也非常简单:

userDao.getUserInfo() >> [user1, user2]

两个右箭头 ”>>” 示意即模仿 getUserInfo 接口的返回后果,再加上应用的 groovy 语言,能够间接应用 ”[]” 中括号示意返回的是 List 类型(具体语法会在下一篇讲到)

三. 单元测试不仅仅是为了达到覆盖率统计,更重要的是验证业务代码的健壮性、逻辑的严谨性以及设计的合理性

在我的项目初期为了赶进度,可能没工夫写单测,或者这个期间写的单测只是为了达到覆盖率要求(因为有些公司在公布前会应用 jacoco 等单测覆盖率工具来设置一个规范,比方新增代码必须达到 80% 的覆盖率能力公布)

再加上传统的单测是应用 java 这种强类型语言写的,以及各种底层接口的 mock 导致写起单测来繁琐费时

这时写的单测代码比拟毛糙,颗粒度比拟大,短少对单测后果值的无效验证,这样的单元测试对代码品质的验证和晋升无奈齐全发挥作用,更多的是为了测试而测试

最初大家不得不承受“尽管写了单测,但却没什么鸟用”的后果

比方上面这段业务代码示例:

void办法,没有返回后果,如何写单测测试这段代码的逻辑是否正确?即如何晓得单测代码是否执行到了 for 循环外面的语句(能够通过查看覆盖率或打断点的形式确认,但这样太麻烦了),如何确保循环外面的金额是否计算正确?

大家能够想下应用 junit 的形式写单元测试如何验证这几点?

/**
 * 用户服务
 * @author 公众号:Java 老 K
 * 集体博客:www.javakk.com
 */
@Service
public class UserService {

    @Autowired
    MoneyDAO moneyDAO;

    /**
     * 依据汇率计算金额
     * @param userVO
     */
    public void setOrderAmountByExchange(UserVO userVO){if(null == userVO.getUserOrders() || userVO.getUserOrders().size() <= 0){return ;}
        for(OrderVO orderVO : userVO.getUserOrders()){BigDecimal amount = orderVO.getAmount();
            // 获取汇率(调用汇率接口)
            BigDecimal exchange = moneyDAO.getExchangeByCountry(userVO.getCountry());
            amount = amount.multiply(exchange); // 依据汇率计算金额
            orderVO.setAmount(amount);
        }
    }
}

应用 Spock 写的话就会不便很多,如下图所示:

其中:

2 * moneyDAO.getExchangeByCountry(_) >> 0.1413 >> 0.1421

这行代码示意在 for 循环中一共调用了 2 次获取汇率的接口,第一次汇率后果是 0.1413,第二次是 0.1421,(模仿汇率接口的实时变动),而后在 with 里验证,相似于 junit 里的 assert 断言,验证汇率折算后的人民币价格是否正确(残缺代码会在后续文章中列出)

这样的益处就是:

晋升单测代码的可控性,不便验证业务代码的逻辑正确和是否正当, 这正是 BDD(行为驱动开发) 思维的一种体现

因为代码的可测试性是掂量代码品质的重要规范, 如果代码不容易测试, 那就要思考重构了, 这也是单元测试的一种正向作用

这一篇文章从 3 个方面比照展现了 Spock 的特点和劣势,前面会具体解说 Spock 的各种用法(联合具体的业务场景),以及 groovy 的一些语法和注意事项

文章起源:http://javakk.com/264.html

正文完
 0