Spock 是非常简洁规范的单元测试框架,网上很多资料都不齐全,例子也很难懂。我自己经过一段时间的学习,梳理了这篇文章,不仅讲解层次递进,而且还有非常简洁明了的例子,小白都能懂!

快速入门 Spock

使用 Spock 非常简单,只需要引入对应的 Spock 依赖包就可以写 Spock 单测代码了。下面我将演示一个使用 Spock 进行单测的最小项目,帮助大家最快上手 Spock。本文档所有例子可在 Github 项目中找到,地址:chenyurong/quick-start-of-spock: 深入浅出 Spock 单测

首先,我们使用 Spring Initializr 初始化一个项目,不需要引入任何依赖应用,这里我命名为 quick-start-of-spock。项目初始化完成之后,在 pom.xml 文件中添加 Spock 依赖,如下代码所示。

<dependency><groupId>org.spockframework</groupId><artifactId>spock-core</artifactId><version>1.2-groovy-2.4</version>
</dependency>

接着,我们编写一个计算器类,用来演示 Spock 单测的使用,代码如下所示。

package tech.shuyi.qsospublic class Calculator {public int add(int num1, int num2) {  return num1 + num2;  }public int sub(int num1, int num2) {  return num1 - num2;  }public int mul(int num1, int num2) {  return num1 * num2;  }public int div(int num1, int num2) {  return num1 / num2;  }
}

接着,我们为 Calculator 生成一个测试类,放在 test 目录下即可,名称命名为 CalculatorTest.groovy,代码如下所示。

package tech.shuyi.qsosimport spock.lang.Specificationclass CalculatorTest extends Specification {Calculator calculator = new Calculator()def "test add method, 1 add 1 should equals 2."() {given: "init input data"def num1 = 1def num2 = 1when: "call add method"def result = calculator.add(num1, num2)then: "result should equals 2"result == 2}def "test sub"() {expect:calculator.sub(5, 4) == 1}def "test mul"() {expect:calculator.mul(5, 4) == 20}def "test div"() {when:calculator.div(1, 0)then:def ex = thrown(ArithmeticException)ex.message == "/ by zero"}
}

这个测试类中,针对 Calculator 类的 4 个加减乘除方法都配置了对应的单测用例。到这里,Spock 的代码就编写完成了。我们直接点击 CalculatorTest 类左边的运行按钮即可运行整个单测用例,如下图所示。

正常情况下,所有单测用例都应该通过测试,都显示绿色的图标,如下图所示。

我们还可以用来计算一下单测覆盖率,运行入口如下图所示。

点击运行之后,会弹出单测覆盖率结果,我这里对所有方法都覆盖了,因此覆盖率是 100%,如下图所示。

到这里,一个最小单元的 Spock 示例项目就结束了。

Spock 语法块

对于 Spock 来说,其最大的特点是使用 give-when-then 等结构来规范了单测的写法,这也是一种非常好的单测规范。因此,了解 Spock 的语法块,知道每个关键词代表的意思就显得非常重要了。

基础语法

对于 Spock 来说,最常用的几个语法块关键词有:

  • given

  • when

  • then

  • and

  • expect

given

given 代码块通常用来进行数据准备,以及准备 mock 数据。例如上面计算器加法单测的例子:

def "test add method, 1 add 1 should equals 2."() {given: "init input data"def num1 = 1def num2 = 1when: "call add method"def result = calculator.add(num1, num2)then: "result should equals 2"result == 2
}

我们在 given 代码块中初始化了 num1 和 num2 两个数据,用于后续计算加法的入参。一般情况下,given 标签都是位于单测的最前面,但 given 代码块并不是必须的。因为如果初始化的数据并不复杂,那么它可以直接被省略。

例如我们这个例子中,初始化的数据只是两个变量,并且数据很简单,那么我们就可以不需要定义变量,而是直接写在入参处,如下代码所示。

def "test add method, 1 add 1 should equals 2."() {when: "call add method"def result = calculator.add(1, 1)then: "result should equals 2"result == 2
}

when

when 代码块主要用于被测试类的调用,例如我们计算器的例子中,我们在 when 代码块中就调用了 Calculator 类的 add 方法,如下代码所示。

def "test add method, 1 add 1 should equals 2."() {given: "init input data"def num1 = 1def num2 = 1when: "call add method"// 调用 Calculator 类的 add 方法def result = calculator.add(num1, num2)then: "result should equals 2"result == 2
}

then

then 代码块主要用于进行结果的判断,例如我们计算器的例子中,我们就在 then 代码块中判断了 result 的结果,如下代码所示。

def "test add method, 1 add 1 should equals 2."() {given: "init input data"def num1 = 1def num2 = 1when: "call add method"def result = calculator.add(num1, num2)then: "result should equals 2"// 判断 result 结果result == 2
}

and

and 代码块主要用于跟在 given、when、then 代码块后,用于将大块的代码分割开来,易于阅读。例如我们计算器的例子,我们假设初始化的数据很多,那么都堆在 given 代码中不易于理解,那么我们可以将其拆分成多个代码块。同理,我们在 when 和 then 代码块中的代码也可以进行同样的拆分,如下代码所示。

def "test add method, 1+1=2, 2+3=5"() {given: "init num1 and num2"def num1 = 1def num2 = 1and: "init num3 and num4"def num3 = 2def num4 = 3when: "call add method(num1, num2)"def result1 = calculator.add(num1, num2)and: "call add method(num3, num4)"def result2 = calculator.add(num3, num4)then: "1 add 1 should equals 2"result1 == 2and: "2 add 3 should equals 5"result2 == 5
}

expect

expect 代码块是 when-then 代码块的精简版本,有时候我们的测试逻辑很简单,并不需要把触发被测试类和校验结果的逻辑分开,这时候就可以用 expect 替代 when-then 代码块。例如计算器的例子中,我们就可以用如下的 expect 代码块来替换 when-then 代码块。

def "test add method, 1 add 1 should equals."() {given: "init input data"def num1 = 1def num2 = 1expect: "1 add 1 should equals 2"calculator.add(num1, num2) == 2
}

到这里,关于 Spock 语法块的基础语法介绍就结束了。

最佳实践

看完了 Spock 语法块的介绍之后,是不是觉得有点懵,不知道应该怎样搭配使用?没关系,其实你用多了之后就会发现,其实常用的搭配就那几种。这里我总结几种代码块的最佳实践,记住这几种就可以了。

given-when-then

given-when-then 组合是使用最多的一种,也是普适性最强的一种。你可以不记得其他的语法块,但这一种你必须记住。对于 given-when-then 组合来说,它的用法如下:

  • given:用来定义初始数据、以及 Mock 信息。

  • when:用来触发被测试类的方法。

  • then:用来进行结果的校验。

根据测试逻辑的复杂程度,我们可以自由地在这三个代码块的后面加上 and 代码块,从而使得代码更加地简洁易读。given-when-then 组合的示例如下代码所示。

def "test add method, 1 add 1 should equals 2."() {given: "init input data"def num1 = 1def num2 = 1when: "call add method"def result = calculator.add(num1, num2)then: "result should equals 2"result == 2
}

given-expect

given-expect 是 given-when-then 的简化版本,主要用于简化代码,提升我们写代码的效率。本质上来说,其就是把 when-then 组合在一起,换成了 expect 代码块。对于 given-expect 组合来说,它的用法如下:

  • given:用来定义初始数据、以及 Mock 信息。

  • expect:用来触发被测试类的方法,并进行结果校验。

如果触发被测试类以及结果校验的逻辑很简单,那么你可以尝试用 given-expect 组合来简化代码。given-expect 组合的示例如下代码所示。

def "test add method, 1 add 1 should equals."() {given: "init input data"def num1 = 1def num2 = 1expect: "1 add 1 should equals 2"calculator.add(num1, num2) == 2
}

更进一步,如果单测逻辑中初始化数据的逻辑也很简单,那么你可以直接省略 given 代码块,直接写一个 expect 代码块即可!

def "test add method, 1 add 1 should equals 2."() {expect: "1 add 1 should equals 2"calculator.add(1, 1) == 2
}

高级语法

where

where 代码块是 Spock 用于简化代码的又一利器,它能以数据表格的形式一次性写多个测试用例。还是拿上面的计算器加法函数的例子,我们可能会测试正数是否运算正确,也需要测试负数是否运算正确。如果没有用 where 代码块,那么我们需要重复写两个测试函数,如下代码所示:

def "test add method, 1 add 1 should equals 2."() {expect: "1 add 1 should equals 2"calculator.add(1, 1) == 2
}
def "test add method, -1 add -1 should equals -2."() {expect: "1 add 1 should equals 2"calculator.add(-1, -1) == -2
}

如果使用了 where 代码块,那么可以将其合并成一个测试函数,如下代码所示:

def "test add method with multi inputs and outputs"() {expect: "1 add 1 should equals 2"calculator.add(num1, num2) == resultwhere: "some possible situation"num1 | num2 || result1    | 1    || 2-1   | -1   || -2
}

上面代码运行的结果如下图所示:

可以看到两个测试用例都整合在一行了,这样不当某行数据出错的时候,我们不知道到底是哪个出错。其实我们可以使用 @Unroll 注解给每个行测试数据起个名字,这样方便后续知道哪个用例出错,如下代码所示:

@Unroll
def "test add method #name"() {expect: "1 add 1 should equals 2"calculator.add(num1, num2) == resultwhere: "some possible situation"name              | num1 | num2 || result"positive number" | 1    | 1    || 2"negative number" | -1   | -1   || -2
}

这样每个测试用例都会独自成为一行,如下图所示:

一般来说 where 代码块可以放在 expect 后,也可以跟在 then 后,其执行效果都一样。

stub

在单测中会有很多外部依赖,我们需要把外部依赖排除掉,其中有一个很常见的场景是:需要让外部接口返回特定的值。而单测中的 stub 就是用来解决这个问题的,通过 stub 可以让外部接口返回特定的值。

说起 stub 这个单词,一开始很不理解。但后面查了查它的英文单词,再联想一下其使用场景,就很容易理解了。stub 英文是树桩的意思,啥是树桩,就是像下面的玩意。单测的 stub 就是在外部依赖接口那里立一个树桩,当你跑到那个位置遇到了桩子(单测执行),就自动弹回来(返回特定值)。

在 Spock 中使用 stub 非常简单,只需要两步即可:

  1. 确定需要 stub 的对象

  2. 指定 stub 对象被调用方法的行为 举个例子,现在我们有一个更加复杂的计算器,里面有一个加法函数。该加法函数调用了开关服务的 isOpen 接口用于判断开关是否打开。当开关打开时,我们需要将最终的结果再乘以 2。当开关服务关闭时,直接返回原来的值。这个复杂计算器类的代码如下所示:

public class ComplexCalculator {SwitchService switchService;public int add(int num1, int num2) {return switchService.isOpen()? (num1 + num2) * 2: num1 + num2;}public void setSwitchService(SwitchService switchService) {this.switchService = switchService;}
}

我们并不知道 SwitchService 的具体逻辑是什么,但我们只知道开关打开时结果乘以 2,开关关闭时返回原来的结果。那么我们如何测试我们的加法函数是否编写正确呢?这时候就需要用到 stub 功能去让 SwitchService 接口返回特定的值,以此来测试我们的 add 函数是否正确了。此时的测试类代码如下所示:

import spock.lang.Specification
import spock.lang.Unrollclass ComplexCalculatorTest extends Specification {@Unrolldef "complex calculator with Stub #name"() {given: "a complex calculator"ComplexCalculator complexCalculator = new ComplexCalculator()and: "stub switch service"// stub a switch service return with isOpenSwitchService switchService = Stub(SwitchService)switchService.isOpen() >> isOpen// set switch service to calculatorcomplexCalculator.setSwitchService(switchService)expect: "should return true"complexCalculator.add(num1, num2) == resultwhere: "possible values"name                | isOpen | num1 | num2 || result"when switch open"  | true   | 2    | 3    || 10"when switch close" | false  | 2    | 3    || 5}
}

如上代码所示,我们在 and 代码块中 stub 了一个 SwitchService 对象,并将其复制给了 ComplexCalculator 对象,对象返回的值取决于 isOpen 属性的值。最后,在 where 代码块里,我们分别测试了开关打开和关闭时的场景。

mock

mock 又是单测中一个非常重要的功能,甚至很多人会把 mock 与 stub 搞混,以为 stub 就是 mock,实际上它们很相似,但又有所区别。应该说:mock 包括了 stub 的所有功能,但是 mock 有 stub 没有的功能,那就是校验 mock 对象的行为。

我们先来说第一个点:mock 包括了 stub 的所有功能,即 mock 也可以插桩返回特定数据。在这个功能上,mock 其用法与 stub 一模一样,你只需要把 Stub 关键词换成 Mock 关键词即可,例如下面的代码与上文 stub 例子中代码的功能是一样的。

@Unroll
def "complex calculator with Mock #name "() {given: "a complex calculator"ComplexCalculator complexCalculator = new ComplexCalculator()and: "stub switch service"// replace Stub with MockSwitchService switchService = Mock(SwitchService)switchService.isOpen() >> isOpencomplexCalculator.setSwitchService(switchService)expect: "should return true"complexCalculator.add(num1, num2) == resultwhere: "possible values"name                | isOpen | num1 | num2 || result"when switch open"  | true   | 2    | 3    || 10"when switch close" | false  | 2    | 3    || 5
}

接着,我们讲第二个点,即:Mock 可以校验对象的行为,而 stub 不行。举个例子,在上面的例子中,我们知道 add () 方法需要去调用 1 次 switchService.isOpen () 方法。但实际上有没有调用,我们其实不知道。

虽然我们可以去看代码,但是如果调用层级和链路很复杂呢?我们还是要一行行、一层层去调用链路吗?这时候 Mock 的校验对象行为功能就发挥出价值了!

@Unroll
def "complex calculator with Mock examine action #name "() {given: "a complex calculator"ComplexCalculator complexCalculator = new ComplexCalculator()and: "stub switch service"SwitchService switchService = Mock(SwitchService)complexCalculator.setSwitchService(switchService)when: "call add method"def realRs = complexCalculator.add(num1, num2)then: "should return true and should call isOpen() only once"// 校验 isOpen() 方法是否只被调用 1 次1 * switchService.isOpen() >> isOpenrealRs == resultwhere: "possible values"name                | isOpen | num1 | num2 || result"when switch open"  | true   | 2    | 3    || 10"when switch close" | false  | 2    | 3    || 5
}

如上代码所示,第 12 行就用于校验 isOpen () 方法是否被调用了 1 次。除了判断是否被调用过之外,Mock 还能判断参数是否是特定类型、是否是特定的值等等。

如果必须要掌握一个功能,那么只掌握 mock 就好。但为了让代码可读性更高,如果只需要返回值,不需要校验对象行为,那还是用 Stub 即可。如果既需要返回值,又需要校验对象行为,那么才用 Mock。

thrown

有时候我们在代码里会抛出异常,那么我们怎么校验抛出异常这种情况呢?Spock 框架提供了 thrown 关键词来对异常抛出做校验。以计算器的例子为例,当我们的分母是 0 的时候会抛出 ArithmeticException 异常,此时我们便可以用 thrown 关键词捕获,如下代码所示。

// 除法函数
public int div(int num1, int num2) {  return num1 / num2;
}  // 测试用例
def "test div"() {when:calculator.div(1, 0)then:def ex = thrown(ArithmeticException)ex.message == "/ by zero"
}

在 then 代码块中,我们用 thrown (ArithmeticException) 表明调用 calculator.div (1, 0) 时会抛出异常,并且用一个 ex 变量接收该异常,随后还对其返回的信息做了校验。

想了解更多与单测相关的知识点?

想与更多小伙伴交流单测?

扫描下方二维码备注(「单测交流」)我拉你入群交流。


推荐阅读

  • 一文带你弄懂 MySQL 的加锁规则!

  • 业务变化快,有必要写单测吗?

  • 单测无用论,这是真的吗?

  • 一文带你搞懂 mmap 技术

  • 找公司 CTO 聊了聊,原来技术总监需要这些能力!

  • PlantUML 入门教程:像写代码一样画图

  • JVM 的栈上分配、TLAB、PLAB 有啥区别?

  • 一文讲清楚 JVM Safe Point 到底是啥!

  • 这个国庆,我去佛山看舞狮,太惊艳!

  • 深入理解 ForkJoinPool:入门、使用、原理

  • 学习性能优化,如何模拟各种故障场景?

可能是全网最好的 Spock 单测入门文章!相关推荐

  1. Spock单测利器的写法

    Spock是国外的测试框架,其设计灵感来自JUnit.Mockito.Groovy,可以用于Java和Groovy应用的测试. Spock简介 最近发现了一款写法简洁高效,一个单测方法可以测试多组测试 ...

  2. 【spock】单测竟然可以如此丝滑

    0. 为什么人人都讨厌写单测 在之前的关于swagger文章里提到过,程序员最讨厌的两件事,一件是别人不写文档,另一件就是自己写文档.这里如果把文档换成单元测试也同样成立. 每个开发人员都明白单元测试 ...

  3. 百度单测生成技术如何召回线上服务的异常问题?

    导读:线上系统异常问题一直以来都是使人"闻风丧胆"的,传统手段在解决这类问题时面临着相应的技术瓶颈.基于此,探索基于单元测试召回异常问题的方法,实现了一套通用且无人参与的单测生成系 ...

  4. 研效优化实践:Python单测——从入门到起飞

    作者:uniquewang,腾讯安全平台后台开发工程师 福生于微,积微成著,一行代码的精心调试,一条指令的细心验证,一个字节的研磨优化,都是影响企业研发效能工程的细节因素.而单元测试,是指针对软件中的 ...

  5. 从头到脚说单测——谈有效的单元测试

    导语 非常幸运的是,从4月份至今,我能够全身心投入到腾讯新闻的单元测试专项任务中,从无知懵懂,到不断深入理解的过程,与开发同学互帮互助,受益匪浅.在此过程中,得到了质量总监.新闻总监和乔帮主的倾囊指导 ...

  6. jacoco + junit + mock 单测没有统计覆盖率问题

    使用junit :直接在pom文件中直接引入: <properties><project.build.sourceEncoding>UTF-8</project.buil ...

  7. Java 单测 回滚

    Java 单测 回滚 Java 在单测的时候 需要做回滚 设置如下: 需要添加以下 注解 在类上 defaultRollback = true : 为 默认全部回滚 defaultRollback = ...

  8. Jest + React Testing Library 单测总结

    大家好,我是若川.持续组织了6个月源码共读活动,感兴趣的可以点此加我微信 ruochuan02 参与,每周大家一起学习200行左右的源码,共同进步.同时极力推荐订阅我写的<学习源码整体架构系列& ...

  9. idea单测覆盖率不显示的问题

    在启动配置里面增加包 包含覆盖率数据是指原始代码所在的包或者类,如果测试文件跟源文件不在一个类下,可以手动添加对应的文件目录,可以只针对某一个类进行添加,这样可以每次调试的时候,最小范围运行单测用例

最新文章

  1. EI:天大王灿+昆士兰郭建华揭示生物气溶胶是猪场耐药基因的重要传播途径
  2. 深度学习:技术原理、迭代路径与局限
  3. 2022-02-25
  4. 读《我编程,我快乐,程序员的职业规划之道》有感
  5. Oracle 联机重做日志文件(ONLINE LOG FILE)
  6. mysql java 分页实体类_Java GUI+mysql+分页查询
  7. CentOS 7安装zabbix-2.4.8监控
  8. WebSocket介绍和Socket的区别
  9. zh-cn en-uk、zh-tw表示语言(文化)代码与国家地区对照表(最全的各国地区对照表)...
  10. 转 Android中this、super的区别
  11. leetcode·双指针
  12. 洛谷4234最小差值生成树 (LCT维护生成树)
  13. 【Uipath杂谈】用Datatable处理数据(二)
  14. 教师利用计算机中的视频图片,多媒体在计算机教学中的作用
  15. 公司最大的内耗,是“人才错配”
  16. Rhino6.9软件安装教程|兼容WIN10
  17. 学 android需要什么基础,学习安卓开发需要什么基础?
  18. Django.db.utils.OperationalError: (1045, Access denied for user 'root'@'localhost' (using passwo...
  19. 【机器学习】Rasa NLU以及Rasa Core概念和语法简介(超详细必看)
  20. 单片机 | 51单片机原理

热门文章

  1. 黎明觉醒测试服服务器维护怎么办,这3种方法,可以获得《黎明觉醒》测试资格,建议收藏!...
  2. Web程序设计基础期末大作业——模仿QQ飞车手游S联赛官网编写的网页
  3. vue-element-admin 登录 / 注销 / 权限验证 篇
  4. InfoQ 最新 Java 发展趋势报告
  5. java sscanf_【转】sscanf函数用法实例
  6. 让业务数据流动起来~
  7. gulp-uglify(压缩js)
  8. Python基础 - 模块 (Module) 和 包(Packages)
  9. 【周光权:利用计算机信息技术实施危害行为定性问题】
  10. linux文件系统基础--VFS中的file、dentry和inode--讲得非常透的一篇文章