背景
单元测试的重要性不言而喻,但在实际开发中单元测试往往都是缺失的,原因有很多,其中比较重要的一点是工期短、写单测耗时长
针对这种问题,为了提高写单测的效率,推荐 Spock 测试框架,改善单测体验、解放生产力~
Spock是什么
官网:https://spockframework.org/
Spock简介
官方介绍:
Spock is a testing and specification framework for Java and Groovy applications. What makes it stand out from the crowd is its beautiful and highly expressive specification language. Thanks to its JUnit runner, Spock is compatible with most IDEs, build tools, and continuous integration servers. Spock is inspired from JUnit, jMock, RSpec, Groovy, Scala, Vulcans, and other fascinating life forms.
简单来说,Spock
是一个测试框架,有以下几个核心特点:
- 可以应用于
java
或groovy
应用的单元测试框架。 - 测试代码使用基于
groovy
语言扩展而成的规范说明语言(specification language
)。 - 遵从 BDD(行为驱动开发)模式,有助于提升代码的质量。
- 通过
junit runner
调用测试,兼容绝大部分junit
的运行场景(ide,构建工具,持续集成等)。 - 框架的设计思路参考了
JUnit
,jMock
,RSpec
,Groovy
,Scala
,Vulcans
……
与其他框架对比
Spock
= 传统测试框架 + Mock
+ BDD
+ 文档化
代码规范化,结构层次清晰
简单易读、可维护性强
基于
Groovy
更快的写单侧漂亮的参数化测试和异常测试
缺点:
偶尔有坑(版本不兼容等)
需要了解
Groovy
语言与其它 java 测试框架风格相差比较大,需要适应
而这些理由比起 Spock
提供的易于开发和维护的单元测试代码来说,是可以忽略的。。。
Groovy
使用 Spock
前先了解下 Groovy
维基百科介绍:
Groovy是Java平台上设计的面向对象编程语言。这门动态语言拥有类似Python、Ruby和Smalltalk中的一些特性,可以作为Java平台的脚本语言使用。
Groovy
的语法与 Java
非常相似,以至于多数的 Java
代码也是正确的 Groovy
代码。Groovy
代码动态的被编译器转换成 Java
字节码。由于其运行在JVM上的特性,Groovy
可以使用其他 Java
语言编写的库。
虽然需要了解 Groovy
,但不用担心,Groovy
是一门比较轻量,学习门槛也比较低的语言,而且最重要的是 Groovy
支持 Java
语法,所以理论上使用纯 Java
写 Groovy
也是可以的~
而且熟悉使用 Spock
后,不仅提升了写单测的速度,还多学了一门脚本语言,两全其美~~
Groovy 语法糖
不用分号
可选择性使用
return
默认采用
public
修饰符
==
与equals
更多语法参考推荐文档。。。
Groovy 推荐文档
https://groovy-lang.org/documentation.html
https://sysgears.com/articles/groovy-differences-java/
https://wiki.jikexueyuan.com/project/groovy-introduction/differences-with-java.html
使用Spock
Spock Web Console
使用姿势
安装 IDE 插件
maven 引用
建议使用 1.3-groovy-2.4 版本,其他版本可能与其他包有兼容问题
1
2
3
4
5
6
7
8
9
10
11
12<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.3-groovy-2.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>1.3-groovy-2.4</version>
<scope>test</scope>
</dependency>创建 Groovy 测试目录
编写测试代码
创建测试类时可以这俩个,Spock Specification
会自动继承 Specification
,Groovy Class
需要自己继承
1 | import spock.lang.Specification |
集成 Spring
和 Junit 集成的方式一样
Spring
@ContextConfiguration(locations = "classpath:spring-context.xml")
SpringBoot
@SpringBootTest
Spock中的概念
Specification
在
Spock
中,待测系统的行为是由规格(specification) 所定义的。在使用Spock
框架编写测试时,测试类需要继承自Specification
类。模板方法
1
2
3
4def setup() {} // run before every feature method
def cleanup() {} // run after every feature method
def setupSpec() {} // run before the first feature method
def cleanupSpec() {} // run after the last feature method和 Junit 对比:
Spock Junit setup() @Before cleanup() @After setupSpec() @BeforeClass cleanupSpec() @AfterClass Feature methods
就是测试类中的测试方法,方法名可以是中文
blocks
每个测试方法又被划分为不同的
block
,不同的block
处于测试执行的不同阶段,在测试运行时,各个block
按照不同的顺序和规则被执行,如下图:Spock
定义了多种标签,去描述代码“应该做什么”,“输入条件是什么”,“输出是否符合预期”,从语义层面规范了代码的编写setup:也可以写成
given
,在这个block
中会放置与这个测试方法相关的初始化程序,可选given:输入条件(前置参数),一般会在这个
block
中定义局部变量,mock
等,可选when、
then
:when
与then
需要搭配使用,一起出现,在when
中执行待测试的函数,在then
中判断是否符合预期expect:可以看做精简版的 when+then
and:衔接上个标签,补充的作用
cleanup:退出前做一些清理工作,如关闭资源等
where:做测试时最复杂的事情之一就是准备测试数据,尤其是要测试边界条件、测试异常分支等,这些都需要在测试之前规划好数据。但是传统的测试框架很难轻松的制造数据,要么依赖反复调用,要么使用其他丑陋的方式;在
Spock
中where
完美解决了这个问题,where
可以说是Spock
的核心,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/**
* where 有两种写法
*/
class SpockDemo extends Specification {
def "max"() {
expect:
Math.max(a, b) == c
where: "多个列使用 | 单竖线隔开,|| 双竖线区分输入和输出变量,即左边是输入值,右边是输出值"
a | b || c
3 | 5 || 5
7 | 0 || 7
0 | 0 || 0
}
}
class SpockDemo extends Specification {
def "max"() {
expect:
Math.max(a, b) == c
where:
a << [3, 7, 0]
b << [5, 0, 0]
c << [5, 7, 0]
}
}
断言
在 then
或 expect
中会默认 assert
所有返回值是 boolean
型的语句,所以在 then
和 expect
语句块中不需要写 assert
如果要在其它地方增加断言,需要显式增加 assert
关键字,如:
1 | def setup() { |
with
with
语句可以验证对象内部的多个属性是否符合预期值
1 | expect: |
异常断言
验证有没有抛出异常,可以用 thrown()
1 | def "test"() { |
要验证没有抛出某种异常,可以用 notThrown()
1 | def "test"() { |
Annotation
注解 | 用途 | 样例 |
---|---|---|
Shared | 在多个测试间共享变量 | @Shared def h2 = new H2Database() |
AutoCleanUp | 测试结束后回收资源,不管是否发生异常等 | @AutoCleanup(“shutdown”) def executor = new Executor() |
Ignore | 忽略这个测试 | |
IgnoreIf | 忽略满足条件的测试 | @IgnoreIf(os.isWindows()) |
IgnoreRest | 只运行这个测试 | |
Requries | 满足设定的条件才运行这个测试 | @Requires({env.containsKey(“HASH_KEY_TO_AUTHENTICATE”)}) |
Unroll | 配合数据表的时候,每行运行一个测试 | |
FailsWith | 运行测试必然抛出某个异常 | |
Issue | 指明这个测试对应某个issue | @Issue(“http://redmine.example.com/issues/2554") |
Timeout | 如果运行时间超过某个阈值,则判定为失败 | @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS) |
Title | 设定一个更容易理解的标题名 | @Title(”测试在》繁忙情况下》发红包”) |
Mock
Spock
自带Mock
功能,使用简单方便,同时也支持扩展第三方Mock
框架,比如PowerMock
SpockMock
Mock
创建一个假对象,验证是否执行了某些操作(在 then
或 expect
语句块中)
创建一个
Mock
对象1
2
3
4
5
6
7
8
9
10
11
12class PublisherSpec extends Specification {
Publisher publisher = new Publisher()
// def subscriber = Mock(Subscriber)
Subscriber subscriber = Mock()
// def subscriber2 = Mock(Subscriber)
Subscriber subscriber2 = Mock()
def setup() {
publisher.subscribers.add(subscriber)
publisher.subscribers.add(subscriber2)
}
}交互验证
1
2
3
4
5
6
7
8def "should send messages to all subscribers"() {
when:
publisher.send("hello")
then:
1 * subscriber.receive("hello")
1 * subscriber2.receive("hello")
}在 publisher 调用 send 时,两个 subscriber 都应该被调用一次 receive(“hello”)
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
191 * subscriber.receive("hello") // exactly one call
0 * subscriber.receive("hello") // zero calls
(1..3) * subscriber.receive("hello") // between one and three calls (inclusive)
(1.._) * subscriber.receive("hello") // at least one call
(_..3) * subscriber.receive("hello") // at most three calls
_ * subscriber.receive("hello") // any number of calls, including zero
1 * subscriber.receive("hello") // an argument that is equal to the String "hello"
1 * subscriber.receive(!"hello") // an argument that is unequal to the String "hello"
1 * subscriber.receive() // the empty argument list (would never match in our example)
1 * subscriber.receive(_) // any single argument (including null)
1 * subscriber.receive(*_) // any argument list (including the empty argument list)
1 * subscriber.receive(!null) // any non-null argument
1 * subscriber.receive(_ as String) // any non-null argument that is-a String
1 * subscriber.receive({ it.size() > 3 }) // an argument that satisfies the given predicate
// (here: message length is greater than 3)
1 * subscriber._(*_) // any method on subscriber, with any argument list
1 * subscriber._ // shortcut for and preferred over the above
1 * _._ // any method call on any mock object
1 * _ // shortcut for and preferred over the above
Stubbing
调用 Mock
对象的某个方法时返回特定的值
调用返回指定值
1
subscriber.receive(_) >> "ok"
调用多次返回不同的值
1
2subscriber.receive(_) >>> ["ok", "error", "error", "ok"]
subscriber.receive(_) >> "ok" >> "error" >> "error" >> "ok"希望抛出异常
1
subscriber.receive(_) >> { throw new InternalError("ouch") }
Mock、Stubbing结合
如果既要判断某个 mock
对象的交互,又希望它返回值的话,可以结合 mock
和 stub
,可以这样:
1 | then: |
一般使用 Spock
自带的 Mock
就够了,但是 Spock
的 Mock
也有着常见缺陷,既不能 Mock
私有方法和静态方法
针对这种情况,业界之前常见的是使用 PowerMock
来 Mock
,但是 PowerMock
上手难度较高、且使用复杂
相比 Spock
+ PowerMock
,推荐 Spock
+ TestableMock
来 Mock
,快速上手、使用简单
TestableMock
TestableMock
,阿里新一代测试工具,一款特立独行的轻量Mock工具。
官网:https://alibaba.github.io/testable-mock/#/
常见 Mock 工具对比:
工具 | 原理 | 最小Mock单元 | 对被Mock方法的限制 | 上手难度 | IDE支持 |
---|---|---|---|---|---|
Mockito | 动态代理 | 类 | 不能Mock私有/静态和构造方法 | 较容易 | 很好 |
Spock | 动态代理 | 类 | 不能Mock私有/静态和构造方法 | 较复杂 | 一般 |
PowerMock | 自定义类加载器 | 类 | 任何方法皆可 | 较复杂 | 较好 |
JMockit | 运行时字节码修改 | 类 | 不能Mock构造方法(new操作符) | 较复杂 | 一般 |
TestableMock | 运行时字节码修改 | 方法 | 任何方法皆可 | 很容易 | 一般 |
TestableMock 功能:
- 快速Mock任意调用:使被测类的任意方法调用快速替换为Mock方法,实现”指哪换哪”,解决传统Mock工具使用繁琐的问题
- 访问被测类私有成员:使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
- 快速构造参数对象:生成任意复杂嵌套的对象实例,并简化其内部成员赋值方式,解决被测方法参数初始化代码冗长的问题
- 辅助测试void方法:利用Mock校验器对方法的内部逻辑进行检查,解决无返回值方法难以实施单元测试的问题
不足:
当前 TestableMock
的主要不足在于,编写 Mock
方法时 IDE 无法即时提示方法参数是否正确匹配。若发现匹配效果不符合预期,需要通过自助问题排查文档提供的方法在运行期进行校验