单元测试-Spock

背景

单元测试的重要性不言而喻,但在实际开发中单元测试往往都是缺失的,原因有很多,其中比较重要的一点是工期短、写单测耗时长

针对这种问题,为了提高写单测的效率,推荐 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 是一个测试框架,有以下几个核心特点:

  • 可以应用于 javagroovy 应用的单元测试框架。
  • 测试代码使用基于 groovy 语言扩展而成的规范说明语言(specification language)。
  • 遵从 BDD(行为驱动开发)模式,有助于提升代码的质量。
  • 通过 junit runner 调用测试,兼容绝大部分 junit 的运行场景(ide,构建工具,持续集成等)。
  • 框架的设计思路参考了 JUnitjMockRSpecGroovyScalaVulcans……

与其他框架对比

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 语法,所以理论上使用纯 JavaGroovy 也是可以的~

而且熟悉使用 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

https://meetspock.appspot.com/

使用姿势

  • 安装 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 会自动继承 SpecificationGroovy Class 需要自己继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import spock.lang.Specification

class SpockDemo extends Specification {
def "test"() {
given: "数据准备"
def list = []

when: "执行需要测试的代码"
list.add("test")

then: "验证执行结果"
!list.empty
stack.size() == 1
}

def "测试"() {
expect:
// ...
}
}

集成 Spring

和 Junit 集成的方式一样

  • Spring

    @ContextConfiguration(locations = "classpath:spring-context.xml")

  • SpringBoot

    @SpringBootTest

Spock中的概念

  • Specification

    Spock 中,待测系统的行为是由规格(specification) 所定义的。在使用 Spock 框架编写测试时,测试类需要继承自 Specification 类。

  • 模板方法

    1
    2
    3
    4
    def 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 等,可选

    whenthenwhenthen 需要搭配使用,一起出现,在 when 中执行待测试的函数,在 then 中判断是否符合预期

    expect:可以看做精简版的 when+then

    and:衔接上个标签,补充的作用

    cleanup:退出前做一些清理工作,如关闭资源等

    where:做测试时最复杂的事情之一就是准备测试数据,尤其是要测试边界条件、测试异常分支等,这些都需要在测试之前规划好数据。但是传统的测试框架很难轻松的制造数据,要么依赖反复调用,要么使用其他丑陋的方式;在 Spockwhere 完美解决了这个问题,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]
    }
    }

断言

thenexpect 中会默认 assert 所有返回值是 boolean 型的语句,所以在 thenexpect 语句块中不需要写 assert

如果要在其它地方增加断言,需要显式增加 assert 关键字,如:

1
2
3
4
def setup() {
stack = new Stack()
assert stack.empty
}

with

with 语句可以验证对象内部的多个属性是否符合预期值

1
2
3
4
5
expect:
with(response) {
code == 0
message == "成功"
}

异常断言

验证有没有抛出异常,可以用 thrown()

1
2
3
4
5
6
7
8
9
10
11
def "test"() {
given:
def str = null

when:
str.add("test")

then:
thrown(NullPointerException)
}

要验证没有抛出某种异常,可以用 notThrown()

1
2
3
4
5
6
7
8
9
10
def "test"() {
given:
def str = []

when:
str.add("test")

then:
notThrown(NullPointerException)
}

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

创建一个假对象,验证是否执行了某些操作(在 thenexpect 语句块中)

  • 创建一个 Mock 对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class 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
    8
    def "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
    19
    1 * 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
    2
    subscriber.receive(_) >>> ["ok", "error", "error", "ok"]
    subscriber.receive(_) >> "ok" >> "error" >> "error" >> "ok"
  • 希望抛出异常

    1
    subscriber.receive(_) >> { throw new InternalError("ouch") }

Mock、Stubbing结合

如果既要判断某个 mock 对象的交互,又希望它返回值的话,可以结合 mockstub,可以这样:

1
2
3
then:
1 * subscriber.receive("message1") >> "ok"
1 * subscriber.receive("message2") >> "fail"

一般使用 Spock 自带的 Mock 就够了,但是 SpockMock 也有着常见缺陷,既不能 Mock 私有方法和静态方法

针对这种情况,业界之前常见的是使用 PowerMockMock,但是 PowerMock 上手难度较高、且使用复杂

相比 Spock + PowerMock,推荐 Spock + TestableMockMock,快速上手、使用简单

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 无法即时提示方法参数是否正确匹配。若发现匹配效果不符合预期,需要通过自助问题排查文档提供的方法在运行期进行校验

推荐文档

分享到: