spock test notes
imports
import spock.lang.*
Specification
class MySpecification extends Specification {
// fields, 每个feature方法都会重新创建
def obj = new ClassUnderSpecification()
def col1 = new Collaborator()
// 只被创建一次
@Shared res = new VeryExpensiveResource()
// constants
static final PI = 3.141592654
// fixture methods
def setup() {} // 每个测试方法之前执行
def cleanup() {} // 每个测试方法之后执行
def setupSpec() {} // 第一个测试方法之前执行
def cleanupSpec() {} // 最后一个测试方法之后执行
// feature methods
def "测试方法-specification"() {
given: "是setup的别名"
def file = new File("/some/path")
def stack = new Stack()
def elem = "push me"
when: "Stimulus"
// then部分, 每个表达式都是个boolean!!!!
then: "Response"
stack.size() == 1
stack.peek() == elem
!stack.empty
thrown(EmptyStackException) // 抛出了指定类型的异常
EmptyStackException e = thrown() // 获得抛出的异常!!!
notThrown(EmptyStackException) // 没有抛出异常
1 * subscriber1.recieve("event") // 交互测试
1 * subscriber2.recieve("event") // 交互测试
expect: "Stimulus + Response"
Math.max(1,2) == 2
cleanup: "Cleanup"
file.delete()
where: "Setup + Stimulus + Response + Cleanup"
}
// help methods
}
Data Driven Testing
适合于纯函数测试
class MathSpec extends Specification {
// 这个annotation将给出所有的报告
@Unroll
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a | b || c
1 | 2 || 2
2 | 1 || 2
1 | 1 || 1
// 也可以用Data Pipe语法 : Collection, String, Iterable
// where:
// a << [ 1,2,1 ]
// b << [ 2,1,1 ]
// c << [ 2,2,2 ]
}
}
多变量的DataPipe
@Shared sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")
def "maximum of two numbers"() {
where:
[a, b, c] << sql.rows("select a, b, c from maxdata")
[x, y, _, z] << sql.rows("select * from datarow") // ignored with _
}
DataTable, DataPipe, Assignment可以共用
...
where:
a | _ // data table
3 | _
7 | _
0 | _
b << [5, 0, 0] // data pipe
c = a > b ? a : b // assignment
交互测试
mock: 描述被测试对象(object under specification)与协作对象(collaborators)之间的交互!!
def subscriber = Mock(Subscriber) //
def Subscriber subscriber1 = Mock() // 推荐!!!
def Subscriber subscriber2 = Mock() //
def "should send messages to all subscribers"() {
when:
publisher.send("hello")
then:
1 * subscriber1.recieve("hello") // subscriber1.recieve("hello") 被调用一次
1 * subscriber2.recieve("hello")
}
// 这里可以这么解读:
// 当publisher发布一个hello消息,
// 则subsriber1与subscriber2都会收到hello消息一次
// 基数限制: Cardinality constraint
(1..3) * subscriber2.recieve("hello") // 1到三次
(1.._) * subscriber2.recieve("hello") // 1次以上
(_..3) * subscriber2.recieve("hello") // 0到三次
// 方法constraints
(_..3) * subscriber2./r.*e/("hello") // 方法满足正则
// 目标constraints:
1 * /^sub*/.recieve("hello")
// 参数constraints
(_..3) * subscriber2.recieve(_ as String) // 参数为字符串
(_..3) * subscriber2.recieve({ it.size() > 3 }) // 参数的size > 3
(_..3) * subscriber2.recieve(!null) // 参数不为null
(_..3) * subscriber2.recieve(_) // 任何参数
(_..3) * subscriber2.recieve(*_) // 任意个参数
(_..3) * subscriber2.recieve(!"hello") // 不是"hello"
// 第三个参数任意, 第四个参数不为null, 第五个参数必须是"abc"或者"def"
1 * process.invoke("ls", "-a", _, !null, { ["abc", "def"].contains(it)} )
// 交互测试断言中有变量, 必须用interaction块
interaction {
def message = "hello"
1 * subscriber1.receive(message)
}
// 注意:
1 * subscriber.status // 1 * subsrcriber.getStatus()
1 * subscriber.setStatus("ok") // setter必须这么写!!!
// 测试invocation order
// 假设发送消息为: "hello" "hello" "goodbye", "hello" "goodbye" "hello"
then:
2 * subscriber.recieve("hello")
1 * subscriber.recieve("goodbye")
then:
1 * subscriber.recieve("hello")
1 * subscriber.recieve("goodbye")
then:
1 * subscriber.recieve("hello")
严格mocking(Strict Mocking)
when:
publisher.publish("hello")
then:
1 * subscriber.receive("hello") // subscriber收到hello消息一次
_ * auditing._ // 允许与auditing有任意的交互
0 * _ // 没有其他交互!!
stub: 设置collaborators对方法调用的表现出指定的响应!
given:
def subscriber = Stub(Subscriber)
Subscriber subscriber = Stub() // 推荐方式!!!!
// 返回固定值
subscriber.recieve(_) >> "abc"
// 返回sequences
subscriber.recieve(_) >>> ["abc", "def"] // 第一次返回"abc", 第二次返回"def"
// 返回计算值
subscriber.recieve(_) >> { args -> args[0].size() > 3 "ok" : "fail" }
// sideeffects
subscriber.recieve(_) >> { throw new InternalError("ouch") } // 执行副作用
// chained
subscriber.recieve(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok" // chained
// 另外一种设置stub的方法!!!
def subscriber = Stub(Subscriber) {
recieve("message1") >> "ok"
recieve("message2") >> "fail"
}
// 依据不同的参数给出不同的行为!!
given:
User user = Stub()
user.updateRoleAndReturnPreviousOne(_) { Role role ->
if ( Role.ADMIN == role) {
throw new IllegalArgumentException()
} else {
return Role.USER
}
}
结合mock与stub
当mocking与stubbing同一个方法调用时, 必须在同一个interaction中指定!!! 如下的方式是错误的。
setup:
subscriber.receive("message1") >> "ok"
when:
publisher.send("message1")
then:
1 * subscriber.receive("message1")
上面的代码执行过程是这样的: then部分的recieve首先被match, 由于这里并没有指定response, 这里的return为nul。 setup部分的没有机会match 正确的做法如下:
then:
1 * subscriber.recieve("message1") >> "ok" // "message1"消息收到一次且处理结果是ok
1 * subscriber.recieve("message2") >> "fail" // "message2"消息收到一次且处理结果是fail
Spyies 用于对具体对象的部分功能覆盖。 比方说A类有x, y, z三个方法,我们想改变z方法的,但x, y方法保持不变
given:
def subscriber = Spy(SubscriberImpl, constructorArgs: ["Fred"])
subscriber.recieve(_) >> {
//
// 调用SubscriberImpl的方法!!! , 这里并没有给callRealMethod传递参数!
// 参数被自动加上
// 如果我们想传递不同的参数给实际方法可以用callRealMethodWithArgs("changeds message")
//
String message -> callRealMethod();
message.size() > 3 ? "ok":"fail"
}
Spy作部分mock(Spy as partial mocks)
// persister现在时被测对象!
def persister = Spy(MessagePersister) {
isPersistable(_) >> true
}
when:
persister.receive(msg")
then:
1 * persister.persist("msg")
总结下Mock, Stub, Spy
从实现上看,mock和stub都是通过创建自己的对象来替代次要测试对象,然后按照测试的需要控制这个对象的行为。
interaction-based => Mock
state-based => Stub
对于mock来说,exception是重中之重:
我们期待方法有没有被调用,
期待适当的参数,
期待调用的次数,
甚至期待多个mock之间的调用顺序。
所有的一切期待都是事先准备好,
在测试过程中和测试结束后验证是否和预期的一致。
而对于stub,
通常都不会关注exception。
虽然理论上某些stub实现也可以通过自己编码的方式增加对expectiation的内容,
比如增加一个计数器,每次调用+1之类,但是实际上极少这样做。
Spy是对实现好的对象的wrapper, spy可以用于覆盖部分
被wrap的对象的行为。
Spy的调用会转发到真实对象的调用!!!!
spy也可以设置交互行为