Spock 测试框架
1. Spock Primer
todo
2. 数据驱动测试
todo
3. 基于交互的测试
class Publisher {
List subscribers = []
void send(String message){
subscribers*.receive(message)
}
}
interface Subscriber {
void receive(String message)
}
class PublisherSpec extends Specification {
Publisher publisher = new Publisher()
}
mock 框架提供了一种方式, 可以描述 spec 对象(即待测对象)及其协作者之间的期望交互,
并且能生成协作者的 mock 实现, 用来核查这个期望.
要测试 send 方法, 得有 Subscriber 类型的协作者.
3.1 创建 mock 对象
创建两个 mock 对象:
def subscriber = Mock(Subscriber)
def subscriber2 = Mock(Subscriber)
// 用下面的方式也是可以的, ide 自动提示功能可能会更好:
// mock 对象的类型会根据变量类型推断出来
Subscriber subscriber = Mock()
Subscriber subscriber2 = Mock()
3.2 把 mock 对象注入到待测对象
class PublisherSpec extends Specification {
Publisher publisher = new Publisher()
Subscriber subscriber = Mock()
Subscriber subscriber2 = Mock()
def setup() {
publisher.subscribers << subscriber // << is a Groovy shorthand for List.add()
publisher.subscribers << subscriber2
}
3.3 Mocking
3.3.1 Interactions
then: block 中, 有两个 interactions, 每个都由 cardinality, target, method, argument
组成.
1 * subscriber.receive("hello")
| | | |
| | | argument constraint
| | method constraint
| target constraint
cardinality
3.3.2 Cardinality(基数)
一个交互(interaction) 的基数描述了期望一个方法调用几次. 它可以是一个固定数字, 也
可以是范围.
1 * subscriber.receive("hello") // 精确的 1 次调用
0 * subscriber.receive("hello") // 0 次
(1..3) * subscriber.receive("hello") // 1 到 3 次 (inclusive)
(1.._) * subscriber.receive("hello") // 至少 1 次
(_..3) * subscriber.receive("hello") // 最多 3 次
_ * subscriber.receive("hello") // any number of calls, including zero
// (rarely needed; see 'Strict Mocking')
3.3.3 Target Constraint
Target constraint 描述了期望哪个 mock 对象来接收方法调用.
1 * subscriber.receive("hello") // a call to 'subscriber'
1 * _.receive("hello") // a call to any mock object
3.3.4 Method Constraint
Method constraint 描述了期望哪个方法被调用
1 * subscriber.receive("hello") // a method named 'receive'
1 * subscriber./r.*e/("hello") // a method whose name matches the given regular expression
当期望一个 getter 方法调用时, 可以用 groovy 的属性访问语法.
1 * subscriber.status // same as: 1 * subscriber.getStatus()
当期望一个 setter 方法调用时, 只能使用方法语法.
1 * subscriber.setStatus("ok") // NOT: 1 * subscriber.status = "ok"
3.3.5 Argument Constraints
描述了方法期望的参数.
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)
当然, 多个参数也是可以的.
处理可变参数时, 跟平时一样用就可以了.
interface VarArgSubscriber {
void receive(String... messages)
}
...
subscriber.receive("hello", "goodbye")
3.3.6 匹配任意方法调用
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
3.3.7 Strick Mocking(严格模式)
略
3.3.8 Where to Declare Interactions
Interactions 不只是可以在 then: block 中出现, 也可以在 setup 方法中出现.
3.3.9 Declaring Interactions at Mock Creation Time (New in 0.7)
如果一个 mock 对象有一组基本的不变的 interactions. 这些 interactions 可以在 mock
对象创建时就声明.
def subscriber = Mock(Subscriber) {
1 * receive("hello")
1 * receive("goodbye")
}
// 也可以这样
Subscriber subscriber = Mock {
1 * receive("hello")
1 * receive("goodbye")
}
这个特性对 带有专门的 Stubs 的 Stubbing 很有用. 注意上面的 interactions 没有 target
contraints, 因为从上下文中可以清楚的知道他们属于哪个 mock 对象.
3.3.10 Grouping Interactions with Same Target (New in 0.7)
with(subscriber) {
1 * receive("hello")
1 * receive("goodbye")
}
一个 with block 还可以用在 grouping conditions.
3.3.11 Mixing Interactions and Conditions
一个 then: block 可以同时包含 interactions 和 conditions. 尽管不是严格必须, 但是
通常的做法是把 interactions 的声明放在 conditions 前面.
when:
publisher.send("hello")
then:
1 * subscriber.receive("hello")
publisher.messageCount == 1
3.3.12 显式定义 interaction 代码块
Internally, Spock must have full information about expected interactions before they take place. So how is it possible for interactions to be declared in a then: block? The answer is that under the hood, Spock moves interactions declared in a then: block to immediately before the preceding when: block. In most cases this works out just fine, but sometimes it can lead to problems:
这样做会出问题
when:
publisher.send("hello")
then:
def message = "hello"
1 * subscriber.receive(message)
Spock 不知道这个交互链接到一个变量声明. 只会把交互这一条语句移到 when: 代码块里去.
所以会产生 MissingPropertyException 异常.
一个解决办法是把 message 声明移到 when: 代码块之前(或者放在 where 代码块中).
另一个解决办法是显式的声明 interaction 代码块
when:
publisher.send("hello")
then:
interaction {
def message = "hello"
1 * subscriber.receive(message)
}
3.3.13 Scope of Interactions
略
3.3.14 Verification of Interactions
主要是基数不匹配
3.3.15 Invocation Order
在一个 then: block 中, invocation 的顺序是不重要的.
例如:
then:
2 * subscriber.receive("hello")
1 * subscriber.receive("goodbye")
可以匹配 "hello" "hello" "goodbye", "hello" "goodbye" "hello", 都没问题.
如果要严格限定顺序的情况, 可以写在两个 then: block 中.
then:
2 * subscriber.receive("hello")
then:
1 * subscriber.receive("goodbye")
3.3.16 Mocking Classes
除了接口, spock 还支持类的 mock. 基本与接口的 mock 是一致的, 除了需要额外的 cglib
库支持. cglib-nodep-2.2 or higher and objenesis-1.2 or higher.
如果 classpath 中没有, 会报错哦. Spock 会告诉你这种错误信息的.
org.spockframework.mock.CannotCreateMockException: Cannot create mock for class cn.codergege.demo.mock.NewSubcriber. Mocking of non-interface types requires a code generation library. Please put byte-buddy-1.4.0 or cglib-nodep-3.2 or higher on the class path.
注意: java 8 只支持 CGLIB 3.2.0 及更高版本.
3.4 Stubbing
todo
3.5 Combining Mocking and Stubbing
1 * subscriber.receive("message1") >> "ok"
1 * subscriber.receive("message2") >> "fail"
当 mocking 和 stubbing 同一个方法时, 他们必须出现在同一个 interaction 中. 下面这
种 Mockito 风格的写法是不能工作的(分开 stubbing 和 mocking 到不同的语句中)
setup:
subscriber.receive("message1") >> "ok"
when:
publisher.send("message1")
then:
1 * subscriber.receive("message1")
这个 receive 方法将在 then: 代码块中第一次匹配. 因为此时没有指定返回值, 所以默认
的方法返回值被返回(这里会返回 null). 因此, 在 setup: 代码块中的 interaction 永远
没有匹配的机会.