乐趣区

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

这是 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

退出移动版