Spock

转载自:https://www.jianshu.com/p/3ee99b8c6be1

介绍

Spock是一个为groovy和java语言应用程序来测试和规范的框架。这个框架的突出点在于它美妙和高效表达规范的语言。得益于JUnit runner,Spock能够在大多数IDE、编译工具、持续集成服务下工作。Spock的灵感源于JUnit,jMock, RSpec, Groovy, Scala, Vulcans以及其他优秀的框架形态。
PS:如果有使用dubbo的同学,这里推荐一个dubbo-postman工具:dubbo-postman

基本概念描述

Spock要求具备基本的groovy和单元测试知识。如果是java程序员,则不需要对groovy感到陌生,因为会java基本就会使用groovy了。

Spock测试类的一般结构

首先看一下测试模板方法的定义和JUnit的对比:

image

Spock基于Groovy,所以像写java测试类一样,需要先创建一个groovy文件。
在一个groovy文件里面的第一行:import spock.lang.*,同时这个类需要继承Specification。
举例:

class MyFirstSpecification extends Specification {
  // 变量字段
  // 测试模板方法
  // 测试方法
  // 测试帮助方法
}

Spock的模板方法说明:

def setupSpec() {}    // runs once -  before the first feature method
def setup() {}        // runs before every feature method
def cleanup() {}      // runs after every feature method
def cleanupSpec() {}  // runs once -  after the last feature method

定义变量:

def obj = new ClassUnderSpecification()
def coll = new Collaborator()

定义测试方法:

def "pushing an element on the stack"() {
  // 测试代码块,最重要的地方
}

代码块结构和测试各个概念阶段的映射:

image

代码块结构在def定义的方法内容里面:

given:
def stack = new Stack()
def elem = "push me"

when:   // 执行需要测试的代码
then:   // 验证执行结果

条件判断:

when:
stack.push(elem)

then:
!stack.empty
stack.size() == 1
stack.peek() == elem

条件不满足的情况输出结果举例:

Condition not satisfied:

stack.size() == 2
|     |      |
|     1      false
[push me]

验证抛出异常举例:

when:
stack.pop()

then:
thrown(EmptyStackException)
stack.empty

访问异常内容:

when:
stack.pop()

then:
def e = thrown(EmptyStackException)
e.cause == null

验证不会抛出异常举例:

//hashmap允许null的key
def "HashMap accepts null key"() {
  given:
  def map = new HashMap()

  when:
  map.put(null, "elem")

  then:
  notThrown(NullPointerException)
}

基于测试的交互,通过mock构造依赖的外部对象

def "events are published to all subscribers"() {
  given:
  def subscriber1 = Mock(Subscriber)//通过Mock构造一个依赖的对象
  def subscriber2 = Mock(Subscriber)
  def publisher = new Publisher()
  publisher.add(subscriber1)
  publisher.add(subscriber2)

  when:
  publisher.fire("event")

  then:
  1 * subscriber1.receive("event")//验证方法名为receive,以及参数"event"为的方法执行一次
  1 * subscriber2.receive("event")
}

expect代码块:

expect:
Math.max(1, 2) == 2

cleanup代码块

given:
def file = new File("/some/path")
file.createNewFile()

// ...

cleanup:
file.delete()

where代码块,可能是Spock最厉害的地方了:

def "computing the maximum of two numbers"() {
  expect:
  Math.max(a, b) == c

  where:
  a << [5, 3]//执行两次测试,值依次为5,3,下面类似
  b << [1, 9]
  c << [5, 9]
}

测试的时候帮助方法的处理:

def "offered PC matches preferred configuration"() {
  when:
  def pc = shop.buyPc()

  then:
  pc.vendor == "Sunny"
  pc.clockRate >= 2333
  pc.ram >= 4096
  pc.os == "Linux"
}

额外定义一个帮助方法:

def "offered PC matches preferred configuration"() {
  when:
  def pc = shop.buyPc()

  then:
  matchesPreferredConfiguration(pc)
}

def matchesPreferredConfiguration(pc) {
  pc.vendor == "Sunny"
  && pc.clockRate >= 2333
  && pc.ram >= 4096
  && pc.os == "Linux"
}

输出结果,结果没有显示具体的哪个判断失败了:

Condition not satisfied:

matchesPreferredConfiguration(pc)
|                             |
false                         ...

使用assert可以达到这个目的:

void matchesPreferredConfiguration(pc) {
  assert pc.vendor == "Sunny"
  assert pc.clockRate >= 2333
  assert pc.ram >= 4096
  assert pc.os == "Linux"
}

最终输出结果,可以看到具体的错误条件:

Condition not satisfied:

assert pc.clockRate >= 2333
       |  |         |
       |  1666      false
       ...

在预期值断言的时候使用with:

def "offered PC matches preferred configuration"() {
  when:
  def pc = shop.buyPc()

  then:
  with(pc) {
    vendor == "Sunny"
    clockRate >= 2333
    ram >= 406
    os == "Linux"
  }
}

使用mock对象的时候也可以用with:

def service = Mock(Service) // has start(), stop(), and doWork() methods
def app = new Application(service) // controls the lifecycle of the service

when:
app.run()

then:
with(service) {
  1 * start()
  1 * doWork()
  1 * stop()
}

当多个预期值一起断言的时候使用verifyAll :

def "offered PC matches preferred configuration"() {
  when:
  def pc = shop.buyPc()

  then:
  verifyAll(pc) {
    vendor == "Sunny"
    clockRate >= 2333
    ram >= 406
    os == "Linux"
  }
}

在没有对象参数的时候也可以这样:

expect:
  verifyAll {
    2 == 2
    4 == 4
  }

文档规范:

given: "open a database connection"
// code goes here

and可以增加多个使代码根据可读性:

given: "open a database connection"
// code goes here

and: "seed the customer table"
// code goes here

and: "seed the product table"
// code goes here

测试行为规范的一般模式:
给定条件....
当怎么样的时候...
应该怎样

given: "an empty bank account"
// ...
when: "the account is credited $10"
// ...

then: "the account's balance is $10"
// ...

扩展注解:
@Timeout 设置方法执行的时间
@Ignore 跳过这个测试方法和JUnit类似
@IgnoreRest 跳过其他所有的测试方法,只执行这个测试方法

数据驱动测试

在where里面可以通过数据表格,数据管道,指定变量三种情况对不同的测试case进行赋值,特别是在当需要测试很多种情况的的时候,这个模式具有非常高的可读性和简洁性:

...
where:
a | _
3 | _
7 | _
0 | _

b << [5, 0, 0]

c = a > b ? a : b

基于测试的交互

场景如下,一个发送者向多个订阅者发送消息

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
  }

期待执行一次接收方法:

def "should send messages to all subscribers"() {
  when:
  publisher.send("hello")

  then:
  1 * subscriber.receive("hello")
  1 * subscriber2.receive("hello")
}

断言表达式解释:

1 * subscriber.receive("hello")
|   |          |       |
|   |          |       关联参数
|   |          关联目标方法
|   关联目标对象
执行次数

在mock创建的时候可以定义多个交互行为:

class MySpec extends Specification {
    Subscriber subscriber = Mock {
        1 * receive("hello")
        1 * receive("goodbye")
    }
}

一个测试代码的多个交互通过when区分:

when:
publisher.send("message1")

then:
1 * subscriber.receive("message1")

when:
publisher.send("message2")

then:
1 * subscriber.receive("message2")

结果输出:

Too many invocations for:

2 * subscriber.receive(_) (3 invocations)

打桩:

很多时候我们测试的时候不需要执行真正的方法,因为在测试代码里面构建一个真实的对象是比较麻烦的,特别是使用一些依赖注入框架的时候,因此有了打桩的功能:

interface Subscriber {
    String receive(String message)
}

模拟返回值:

subscriber.receive(_) >> "ok"

表达式解释:

subscriber.receive(_) >> "ok"
|          |       |     |
|          |       |     任何参数的这个方法调用都会返回"ok"
|          |       argument constraint
|          method constraint
target constraint

需要调用多次返回不同结果的时候可以这样定义:

subscriber.receive(_) >>> ["ok", "error", "error", "ok"]

有时候我们测试的一个方法调用了类的其他方法,而其他的方法可能依赖了外部的对象,为了避免引入外部对象,可以使用部分mock功能减少构建外部对象的繁琐:

// this is now the object under specification, not a collaborator
MessagePersister persister = Spy {
  // 这个方法的任意参数调用都返回true
  isPersistable(_) >> true
}

when:
persister.receive("msg")

then:
// when里面的receive会调用persist,前提是isPersistable是true,因为mock的值是true,这个方法一定会被调用
1 * persister.persist("msg")

和springboot集成

spring本身提供了spring-test测试模块,需要和Spock一起使用的话,只需要在Specification类上面加上SpringApplicationConfiguration这个注解就可以了,如果是mvc应用还需加上WebAppConfiguration这个,其他的使用和原来一样。

maven配置

在pom.xml里面添加

<!-- Spock依赖 -->
    <dependency>
      <groupId>org.spockframework</groupId>
      <artifactId>spock-core</artifactId>
      <version>1.2-groovy-2.4</version>
      <scope>test</scope>
    </dependency>
    <!-- Spock需要的groovy依赖 -->
    <dependency> 
      <groupId>org.codehaus.groovy</groupId>
      <artifactId>groovy-all</artifactId>
      <version>2.4.15</version>
    </dependency>

<!--...... -->
<!--编译插件配置: -->
 <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.20.1</version>
        <configuration>
          <useFile>false</useFile>
          <includes>
            <include>**/*Test.java</include><!--指定JUnit的测试类名称后缀-->
            <include>**/*Spec.java</include><!--指定Spock的测试类名称后缀-->
          </includes>
        </configuration>
      </plugin>

总结

Spock整体上是一个规范描述型的测试框架,测试代码非常易读。对于java程序员,上手非常容易,在学习的时候可以先在一个项目上使用,当你发下它的优点的时候,你会尽可能的把其他的测试代码也改成用Spock来写,这个过程可能会花费你更多的时间,但是当你熟悉了以后,它带给你的收益一定是值得的。不要犹豫,学习新框架最快的方式就是马上动手。

参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351