简介
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 依赖引入
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.0-groovy-2.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>2.4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.12</version>
<type>pom</type>
<scope>test</scope>
</dependency>
<dependency> <!-- enables mocking of classes (in addition to interfaces) -->
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.1</version>
<scope>test</scope>
</dependency>
1.2.定义一个Spock测试类
创建类的时候选择Groovy Class
class MyFirstSpec extends Specification {
....
}
类Specification
包含许多用于编写规范的有用方法。此外,它指示 JUnit 使用Sputnik
Spock 的 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 将变量定义为共享变量
*/
@Shared data="testseeee"
/**
* 在每个def方法运行前给data赋值
*/
void setup(){
data="aaaaa"
}
/**
* 测试方法
* @return
*/
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
}
/**
* 异常捕获,相比较与try-catch,spock中有了新的异常捕获方式
* thrown()
* 通过使用notThrown(),表示不应抛出的异常
*/
def "stackExceptionCatch"(){
given:"initData"
def stack=new Stack()
when:"exec"
stack.pop()
then:"assert"
def e=thrown(EmptyStackException)
e.cause==null
}
/**
* where块的使用,可用于数据驱动的测试
* @Unroll 对于where的每个情况都生成一个单测用例,不加该注解则认为这两种情况只是一个单测示例
* #a |#b ||#c 用于输出参数值,让单测示例更易看
*/
@Unroll
def "math max use where #a |#b ||#c"(){
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 | 释放特性方法使用的任何资源 | 一个 对象级规范通常不需要 |
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 的模拟框架可以轻松地仅描述与交互相关的内容,避免过度规范的陷阱。
三、Mock,stub,spy
stub
stub只是简单的生成一个目标类的代理类,关注重点为方法的返回,对于方法执行的次数等不关注。
def venderWorkOrderCmdRpc = Stub(VenderWorkOrderCmdRpc)
Mock
在stub的基础上有了方法执行次数的关注
given:
subscriber.receive("message1") >> "ok"
when:
publisher.send("message1")
then:
1 * subscriber.receive("message1") //标识该方法应该只执行一次 ,0*标识一次也不执行
如果stub的对象添加方法执行次数的判断会抛出InvalidSpecException的异常。
spy
spy总是基于真实的对象。因此,必须提供类类型而不是接口类型,以及该类型的任何构造函数参数。如果未提供构造函数参数,则将使用该类型的无参数构造函数。
WorkOrderCreateCheckAbilityImpl workOrderCreateCheckAbility = Spy()
四、参考文献
Spock美团技术实践总结:Spock单元测试框架介绍以及在美团优选的实践 - 美团技术团队
Spock单元测试框架保姆级教程:https://javakk.com/category/spock/page/2
Spock官方文档:Spock Framework Reference Documentation