背景
单元测试是保证我们写的代码是我们想要的结果的最有效的办法。根据下面的数据图统计,单元测试从长期来看也有很大的收益。
单元测试收益:
- 它是最容易保证代码覆盖率达到100%的测试。
- 可以⼤幅降低上线时的紧张指数。
- 单元测试能更快地发现问题。
- 单元测试的性价比最高,因为错误发现的越晚,修复它的成本就越高,而且难度呈指数式增长,所以我们要尽早地进行测试。
- 编码人员,一般也是单元测试的主要执行者,是唯一能够做到生产出无缺陷程序的人,其他任何人都无法做到这一点。
- 有助于源码的优化,使之更加规范,快速反馈,可以放心进行重构。
我们都知道单元测试的好处,但是我们遇到的项目还是很多都缺乏单测,一方面是因为单测耗时,一方面也是因为业务的复杂性导致单测难以进行,最长见还是任务重、工期紧,或者干脆就不写了。
为什么要用Spock,和JUnit有什么区别
Spock是一款国外优秀的测试框架,基于BDD(行为驱动开发)思想实现,功能非常强大。Spock结合Groovy动态语言的特点,提供了各种标签,并采用简单、通用、结构化的描述语言,让编写测试代码更加简洁、高效。官方介绍:https://spockframework.org/
Spock的主要特点
- 让测试代码更规范,内置多种标签来规范单元测试代码的语义,测试代码结构清晰,更具可读性,降低后期维护难度。
- 提供多种标签,比如:given、when、then、expect、where、with、thrown......帮助我们应对复杂的测试场景。
- 使用Groovy这种动态语言来编写测试代码,可以让我们编写的测试代码更简洁,适合敏捷开发,提高编写单元测试代码的效率。
- 遵从BDD(行为驱动开发)模式,有助于提升代码的质量。
- IDE兼容性好,自带Mock功能。
现有的单测框架比如junit、jmock、mockito都是相对独立的工具,针对不同的业务场景提供特定的解决方案。在业务场景中有很多第三方服务的依赖,比如微服务的调用、数据库、redis等的存储,我们需要将这些依赖mock掉去验证我们代码的逻辑是否正确。JMock或Mockito虽然提供了mock功能,可以把接口等依赖屏蔽掉,但不提供对静态类静态方法的mock,PowerMock或Jmockit虽然提供静态类和方法的mock,但它们之间需要整合(junit+mockito+powermock),语法繁琐,而且这些工具并没有告诉你**"单元测试代码到底应该怎么写?"**
Spock通过提供规范描述,定义多种标签(given、when、then、where等)去描述代码"应该做什么",输入条件是什么,输出是否符合预期,从语义层面规范代码的编写。
Spock使用简单介绍
基本概念
Spock定义了几个构造块,一段单测也是由这几个构造块组合完成的。
Spock主要提供了如下基本构造块:
- given: mock单测中指定mock数据
- when: 触发行为,比如调用指定方法或函数
- then: 做出断言表达式
- expect: 期望的行为,when-then的精简版
- where: 以表格的形式提供测试数据集合
- thrown: 如果在when方法中抛出了异常,则在这个子句中会捕获到异常并返回
- def setup() {} :每个测试运行前的启动方法
- def cleanup() {} : 每个测试运行后的清理方法
- def setupSpec() {} : 第一个测试运行前的启动方法
- def cleanupSpec() {} : 最后一个测试运行后的清理方法
通过这些标签从行为上规范单测代码,每一种标签对应一种语义,让我们的单测代码结构具有层次感,功能模块划分清晰,便于后期维护
IntelliJ IDEA支持format(opt+cmd+L)格式化快捷键,因为表格列的长度不一样,手动对齐比较麻烦。
实际案例
java
/**
* 身份证号码工具类<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;
}
}
下面案例是测试断言的比较器的单测
运行结果:
代码简洁清晰
安装环境配置&第三方依赖
添加依赖
xml
<!-- spock -->
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>2.4-M1-groovy-4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>2.4-M1-groovy-4.0</version>
<scope>test</scope>
</dependency>
If Esle 多分支场景测试
使用expect+where
expect相当于when+then的精简版
@Unroll注解表示展开where标签下面的每一行测试,作为单独的case跑
异常测试
使用thrown
有些方法会根据不同的场景抛出不同的异常,Spock内置thrown()方法,可以捕获调用业务代码抛出的预期异常并验证,再结合where表格的功能,可以很方便的覆盖多种自定义业务异常,代码如下:
Void方法测试
没有返回值的方法该怎么测试呢,一种有效的测试方式,就是验证方法内部逻辑和流程是否符合预期,比如:
- 应该走到哪个分支逻辑?
- 是否执行了这一行代码?
- for循环中的代码执行了几次?
- 变量在方法内部的变化情况?
静态方法测试
动态Mock
具体场景介绍
使用Spock简化测试代码
基本构造块####
Spock主要提供了如下基本构造块:
- where: 以表格的形式提供测试数据集合
- when: 触发行为,比如调用指定方法或函数
- then: 做出断言表达式
- expect: 期望的行为,when-then的精简版
- given: mock单测中指定mock数据
- thrown: 如果在when方法中抛出了异常,则在这个子句中会捕获到异常并返回
- def setup() {} :每个测试运行前的启动方法
- def cleanup() {} : 每个测试运行后的清理方法
- def setupSpec() {} : 第一个测试运行前的启动方法
- def cleanupSpec() {} : 最后一个测试运行后的清理方法
了解基本构造块的用途后,可以组合它们来编写单测。
参考资料
美团 https://tech.meituan.com/2021/08/06/spock-practice-in-meituan.html
老K的Java博客:https://javakk.com/category/spock
Spring mock: https://spockframework.org/spock/docs/2.2-SNAPSHOT/module_spring.html