基于Spock框架进行单元测试
以Spock测试框架为例,实现外部环境零依赖。(不依赖Spring/db等)
Spock
基于groovy的单元测试框架。
Specification
在Spock中,待测系统(SUT)的行为是由规格(specification) 所定义的。在使用Spock框架编写测试时,测试类需要继承自Specification类。
Fixture Methods
def setup() {} :每个测试运行前的启动方法
def cleanup() {} : 每个测试运行后的清理方法
def setupSpec() {} : 第一个测试运行前的启动方法
def cleanupSpec() {} : 最后一个测试运行后的清理方法
Feature methods
这是Spock规格(Specification)的核心,其描述了SUT应具备的各项行为。每个Specification都会包含一组相关的Feature methods,如要测试1+1是否等于2,可以编写一个函数:
class DemoTest extends Specification { def setup() { } def "sum should return param1+param2" (){ expect: sum.sum(1,1)==2 } }
blocks
每个feature method又被划分为不同的block,不同的block处于测试执行的不同阶段,在测试运行时,各个block按照不同的顺序和规则被执行,如下图:

where: 以表格的形式提供测试数据集合 when: 触发行为,比如调用指定方法或函数
then: 做出断言表达式
expect: 期望的行为,when-then的精简版
setup/given: 数据准备模块,mock单测中指定mock数据
cleanup: 释放资源模块
def "HashMap accepts null key"() { given: def map = new HashMap() when: map.put(null, "elem") then: notThrown(NullPointerException) }
Where Blocks
//多值验证传统写法 class MathSpec extends Specification { def "maximum of two numbers"() { expect: // exercise math method for a few different inputs Math.max(1, 3) == 3 Math.max(7, 4) == 7 Math.max(0, 0) == 0 } } //通过where block可以让上面的测试实现起来变得非常优雅,此例子实际会跑三次测试 classDataDrivenextendsSpecification{ def"maximum of two numbers"(){ expect: Math.max(a,b)==c where: a|b||c 3|5||5 7|0||7 0|0||0 } } //可以为标记@Unroll的方法声明动态的spec名,运行时,名称会被替换为实际的参数值。 class DataDriven extends Specification { @Unroll def "maximum of #a and #b should be #c"() { expect: Math.max(a, b) == c where: a | b || c 3 | 5 || 5 7 | 0 || 7 0 | 0 || 0 } }
spock demo
待测试代码:
public class Publisher { List<Subscriber> subscribers = new ArrayList<>(); void send(String message) { for (Subscriber subscriber : subscribers) { String ret = subscriber.receive(message); if (!"ok".equals(ret)) { throw new RuntimeException("发送失败"); } } } } public interface Subscriber { String receive(String message); } public class SubScriber1 implements Subscriber { @Override public String receive(String message) { System.out.println("subScriber1 receive message:" + message); return "ok"; } } public class SubScriber2 implements Subscriber { @Override public String receive(String message) { System.out.println("subScriber2 receive message:" + message); return "ok"; } }
Stubbing
stub存在的意图是为了让测试对象可以正常的执行,其实现一般会硬编码一些输入和输出。
class PublisherTest extends Specification { Publisher publisher = new Publisher() Subscriber subscriber1 = Mock(Subscriber) Subscriber subscriber2 = Mock(Subscriber) def setup() { publisher.subscribers.add(subscriber1) publisher.subscribers.add(subscriber2) } def "should send messages to all subscribers with return val"() { when: publisher.send("hello") then: subscriber1.receive(_) >> "ok" subscriber2.receive("hello") >> "ok" notThrown(RuntimeException) } }
多种返回值形式
//多次调用不同的值,返回数组 subscriber.receive(_) >>> ["ok", "error", "error", "ok"] //抛出异常 subscriber.receive(_) >> { throw new InternalError("ouch") } //Method responses can be chained: subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"`
Mocking
mock除了保证stub的功能之外,还可深入的模拟对象之间的交互方式,如:调用次数约束、方法名称约束、参数约束等。
class PublisherTest extends Specification { Publisher publisher = new Publisher() Subscriber subscriber1 = Mock(Subscriber) Subscriber subscriber2 = Mock(Subscriber) def setup() { publisher.subscribers.add(subscriber1) publisher.subscribers.add(subscriber2) } def "should send messages to all subscribers"() { when: publisher.send("hello") then: 1 * subscriber1.receive("hello") 0 * subscriber2.receive("hello") thrown(RuntimeException) } }
表达式中的次数、对象、函数和参数部分都可以灵活定义
1 * subscriber.receive("hello") // exactly one call
0 * subscriber.receive("hello") // zero calls
(1..3) * subscriber.receive("hello") // between one and three calls (inclusive)
(1.._) * subscriber.receive("hello") // at least one call
(_..3) * subscriber.receive("hello") // at most three calls
_ * subscriber.receive("hello") // any number of calls, including zero
1 * subscriber.receive("hello") // a call to 'subscriber'
1 * _.receive("hello") // a call to any mock object
1 * subscriber.receive("hello") // a method named 'receive'
1 * subscriber./r.*e/("hello") // a method whose name matches the given regular expression
1 * subscriber.status // same as: 1 * subscriber.getStatus()
1 * subscriber.setStatus("ok") // NOT: 1 * subscriber.status = "ok"
1 * subscriber.receive("hello") // an argument that is equal to the String "hello"
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(endsWith("lo")) // any non-null argument that is-a String
1 * subscriber.receive({ it.size() > 3 && it.contains('a') })
//Argument constraints work as expected for methods with multiple arguments
1 * process.invoke("ls", "-a", _, !null, { ["abcdefghiklmnopqrstuwx1"].contains(it) })
Mocking & Stubbing
def "should send messages to all subscribers with return val 1"() { when: publisher.send("hello") then: 1 * subscriber1.receive("hello") >> "ok" 1 * subscriber2.receive("hello") >> "ok" notThrown(RuntimeException) }
Mocking & Stubbing & 验证入参
class PublisherTest extends Specification { Publisher publisher = new Publisher() Subscriber subscriber1 = Mock(Subscriber) Subscriber subscriber2 = Mock(Subscriber) def setup() { publisher.subscribers.add(subscriber1) publisher.subscribers.add(subscriber2) } def "should send messages to all subscribers with return val and unknow args"() { given: String message1 = null String message2 = null 1 * subscriber1.receive(_) >> { arguments -> Object obj = arguments[0] message1 = (String) obj return "ok" } 1 * subscriber2.receive(_) >> { arguments -> Object obj = arguments[0] message2 = (String) obj return "ok" } when: publisher.send("hello word") then: message1 == "hello word" message2 == "hello word" notThrown(RuntimeException) } }

浙公网安备 33010602011771号