目录
简介
为什么要用Spock
一、入门
1.1 依赖引入
1.2.定义一个Spock测试类
1.3一个简单的测试方法
1.4With与VerifyAll
二、Mock
三、参考文献
简介
Spock 是用于 Java 和 Groovy 应用程序的测试和规范框架。使它从人群中脱颖而出的是其美丽且极具表现力的规范语言。由于其 JUnit 运行器,Spock 与大多数 IDE、构建工具和持续集成服务器兼容。Spock 的灵感来自JUnit、 jMock、RSpec、Groovy、Scala、 Vulcans和其他迷人的生命形式。
为什么要用Spock
总的来说,JUnit、jMock、Mockito都是相对独立的工具,只是针对不同的业务场景提供特定的解决方案。其中JUnit单纯用于测试,并不提供Mock功能。
我们的服务大部分是分布式微服务架构。服务与服务之间通常都是通过接口的方式进行交互。即使在同一个服务内也会分为多个模块,业务功能需要依赖下游接口的返回数据,才能继续后面的处理流程。这里的下游不限于接口,还包括中间件数据存储比如Squirrel、DB、MCC配置中心等等,所以如果想要测试自己的代码逻辑,就必须把这些依赖项Mock掉。因为如果下游接口不稳定可能会影响我们代码的测试结果,让下游接口返回指定的结果集(事先准备好的数据),这样才能验证我们的代码是否正确,是否符合逻辑结果的预期。
尽管jMock、Mockito提供了Mock功能,可以把接口等依赖屏蔽掉,但不能对静态方法Mock。虽然PowerMock、jMockit能够提供静态方法的Mock,但它们之间也需要配合(JUnit + Mockito PowerMock)使用,并且语法上比较繁琐。工具多了就会导致不同的人写出的单元测试代码“五花八门”,风格相差较大。
Spock通过提供规范性的描述,定义多种标签(given、when、then、where等),去描述代码“应该做什么”,“输入条件是什么”,“输出是否符合预期”,从语义层面规范了代码的编写。
Spock自带Mock功能,使用简单方便(也支持扩展其他Mock框架,比如PowerMock),再加上Groovy动态语言的强大语法,能写出简洁高效的测试代码,同时能方便直观地验证业务代码的行为流转,增强工程师对代码执行逻辑的可控性。
一、入门
1.1 依赖引入
org.spockframework
spock-core
1.0-groovy-2.4
test
org.codehaus.groovy
groovy
2.4.12
test
org.codehaus.groovy
groovy-all
2.4.12
pom
test
cglib
cglib-nodep
3.1
test
1.2.定义一个Spock测试类
org.spockframework spock-core1.0-groovy-2.4 test org.codehaus.groovy groovy2.4.12 test org.codehaus.groovy groovy-all2.4.12 pom test cglib cglib-nodep3.1 test
1.2.定义一个Spock测试类
创建类的时候选择Groovy Class
class MyFirstSpec extends Specification {
....
}
类Specification包含许多用于编写规范的有用方法。此外,它指示 JUnit 使用SputnikSpock 的 JUnit运行器运行规范。多亏了 Sputnik,大多数现代 Java IDE 和构建工具都可以运行 Spock 规范。
我们在给测试类命名时通常以Specification或Spec结尾,以标识出类为Spock测试类。
常用方法介绍:
def setupSpec() {} // 运行一次 - 在第一个def方法运行之前
setup() {} // 在每个def方法运行之前运行
cleanup() {} // 在每个def方法运行之后运行
cleanupSpec() {} // 运行一次 - 在最后一个def方法运行之后
Junit与Spock方法的一个映射关系
1.3一个简单的测试方法
class MyFirstSpec extends Specification {
@Shared data="testseeee"
void setup(){
data="aaaaa"
}
def "use when"(){
//初始化数据
given:"initData"
def a=1
def b=2
//运行
when:"exec"
def x=Math.max(a,b)
//校验
then:"assert"
x==2
data=="aaaaa"
}
def "use expect"(){
expect:""
Math.max(1,2)==2
}
def "stackExceptionCatch"(){
given:"initData"
def stack=new Stack()
when:"exec"
stack.pop()
then:"assert"
def e=thrown(EmptyStackException)
e.cause==null
}
def "math max use where"(){
expect:"exec"
c==Math.max(a,b)
where:"assert"
a|b||c
1|2||2
3|2||3
}
}
Spock 内置支持实现功能方法的每个概念阶段。为此,特征方法被构造成所谓的块。块以标签开始,并延伸到下一个块的开头,或方法的结尾。有6种模块:given,when,then,expect,cleanup,和where块。方法开头和第一个显式块之间的任何语句都属于隐式given块。
下图演示了块如何映射到特征方法的概念阶段。该where区块有一个特殊的作用,很快就会揭晓。但首先,让我们仔细看看其他块。
| 块名 | 作用 | 说明 |
| given | 输入条件(前置参数) | 前面不能有其他块,也不能重复。一个given块不具有任何特殊的语义。该given:标签是可选的并且可以省略,导致隐式 given块。最初,别名setup:是首选的块名称,但使用given:通常会导致更易读的功能方法描述(参见规范作为文档)。 |
| when | 执行行为 | 在when和then块总是一起出现。他们描述了一种刺激和预期的反应。虽然when 块可以包含任意代码,但then块仅限于条件、异常条件、交互和变量定义。一个特征方法可能包含多对when-then块。 |
| then | 输出条件,验证结果 | |
| and | 衔接上个标签,补充作用 | |
expect | 类似when+then的结合 | 一个expect块被比较有限then的,因为它可能只包含条件和变量定义块。在更自然地用单个表达式描述刺激和预期反应的情况下,它很有用 |
| cleanup | 释放特性方法使用的任何资源 | 一个cleanup块后面只能跟一个where块,不能重复。与cleanup方法一样,它用于释放特性方法使用的任何资源,即使特性方法(的前一部分)产生了异常,它也会运行。因此,cleanup必须对块进行防御性编码;在最坏的情况下,它必须优雅地处理特征方法中的第一条语句抛出异常的情况,并且所有局部变量仍然具有其默认值。 对象级规范通常不需要cleanup方法,因为它们消耗的唯一资源是内存,垃圾收集器会自动回收内存。然而,更粗粒度的规范可能使用cleanup 块来清理文件系统、关闭数据库连接或关闭网络服务。 |
| where | 使用不同的输入和预期结果多次执行相同的测试代码,主要用于数据驱动的测试 | |
1.4With与VerifyAll
def "offered PC matches preferred configuration"() {
when:
def pc = shop.buyPc()
then:
with(pc) {
vendor == "Sunny"
clockRate >= 2333
ram >= 406
os == "Linux"
}
}
您可以使用一种with(target, closure)方法与正在验证的对象进行交互,当pc对象为null时会抛出异常。这在then和expect块中特别有用。
正常期望在第一个失败的断言上无法通过测试。有时在测试失败之前收集这些失败以获得更多信息是有帮助的,这种行为也称为软断言。该verifyAll方法可以像这样使用with
def "offered PC matches preferred configuration"() {
when:
def pc = shop.buyPc()
then:
verifyAll(pc) {
vendor == "Sunny"
clockRate >= 2333
ram >= 406
os == "Linux"
}
}
二、Mock
在我们测试的过程中很多资源是无法获取或者说无法直接使用的,比如我们调用一个三方的接口,其实我们是无法确认对方的返回内容永不变动的,但是这种不稳定因素就会对我们的测试结果产生影响,所以就需要我们自己去模拟一些对象方法调用或接口的返回,Mock就此登场。就我个人使用而言感觉Mock与@MockBean的作用是相似的。
class PublisherSpec extends Specification {
//模拟接口
def testMock=Mock(TestMock.class)
def room=new Room(testMock:testMock)
def "test mock"(){
given:"initData"
def name="张三"
and:"mock"
//模拟接口调用的返回
testMock.getName()>>name
when:"exec"
def result=room.get(1)
then:"assert"
assert result=="张三"
}
}
测试相关类及接口定义:
public class Room {
public TestMock testMock;
public String get(Integer index){
return testMock.getName();
}
}
public interface TestMock {
String getName();
}
在这里尽管TestMock接口并没有对应的实现类,但是我们还是可以使用该类的方法, 这是因为与大多数 Java模拟框架一样,Spock 使用 JDK 动态代理(模拟接口时)和Byte Buddy或CGLIB代理(模拟类时)在运行时生成模拟实现。
与 Mockito 一样,我们坚信模拟框架默认应该是宽松的。这意味着对模拟对象的意外方法调用(或者,换句话说,与手头测试无关的交互)被允许并以默认响应回答。相反,像 EasyMock 和 JMock 这样的模拟框架在默认情况下是严格的,并且会为每个意外的方法调用抛出异常。虽然严格强制严格,但它也可能导致过度规范,导致脆弱的测试在每次其他内部代码更改时失败。Spock 的模拟框架可以轻松地仅描述与交互相关的内容,避免过度规范的陷阱。
三、参考文献
Spock美团技术实践总结:Spock单元测试框架介绍以及在美团优选的实践 - 美团技术团队
Spock单元测试框架保姆级教程:https://javakk.com/category/spock/page/2
Spock官方文档:Spock framework Reference documentation



