使用Spock框架进行单元测试

1.关于单元测试

不少人一谈到单元测试就会想到xUnit框架。对于一些java新人来讲,会用jUnit就是会写单元测试,高级点的会捣鼓一下testng,而后就认为本身掌握了单元测试。html

而实际上,不少人不怎么会写单元测试,甚至不知道单元测试到底是干什么的。写单元测试要比写代码要难上许多,而这里说的难度跟框架没什么关系。java

因此,在开始介绍spock以前,须要先抛开框架,谈谈单元测试自己的事情。在理解了单元测试以后才能更清楚spock框架是什么,以及它否可以更优雅的解决你的问题。node

1.1.1.单元测试是什么

写代码免不了要作测试,测试有不少种,对于java来讲,最初级的就是写个main函数运行一下看看结果,高级的能够用各类高大上的复杂的测试系统。每种测试都有它的关注点,好比测试功能是否是正确,或者运行状态稳不稳定,或者能承受多少负载压力,等等。git

那么所谓的单元测试是什么?这里直接引用维基百科上的词条说明:程序员

单元测试(又称为模块测试, Unit Testing)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工做。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。github

因此,我眼中的“合格的”单元测试须要知足几个条件:spring

  1. 测试的是一个代码单元内部的逻辑,而不是各模块之间的交互。
  2. 无依赖,不须要实际运行环境就能够测试代码。
  3. 运行效率高,能够随时执行。

1.1.2.单元测试的定位

了解了单元测试是什么以后,第二个问题就是:单元测试是用来作什么的?数据库

不少人第一反应是“看看程序有没有问题”,或者“确保没有bug”。单元测试确实能够测试程序有没有问题,可是,从我我的编程的经验来看,大部分状况下只是使用单元测试来“看看程序有没有问题”的话,效率反而不如把程序运行起来直接查看结果。缘由有两个:express

  1. 单元测试要写额外的代码,而不写单元测试,直接运行程序也能够测试程序有没有问题。
  2. 即便经过了单元测试,程序在实际运行的时候仍然有可能出问题。

可是,不少时候直接启动程序测试会比较慢,因此一些同窗为了解决这个问题,采用了一个折中的办法:只加载要测试的模块和它全部的依赖模块,好比在测试时只加载这个模块相关的spring的配置文件。这时所谓的单元测试其实是用xUnit框架运行的集成测试,并无体现“单元”的概念。apache

而关于“纯粹的单元测试”在介绍语言或者框架的书里不多被提起,反而是介绍重构或者敏捷开发的书里常常会看到各类各样的关于单元测试的介绍。

在这里我总结了一下几个比较常见的单元测试的几个典型场景:

  1. 开发前写单元测试,经过测试描述需求,由测试驱动开发。
  2. 在开发过程当中及时获得反馈,提早发现问题。
  3. 应用于自动化构建或持续集成流程,对每次代码修改作回归测试。
  4. 做为重构的基础,验证重构是否可靠。

还有最重要的一点:编写单元测试的难易程度可以直接反应出代码的设计水平,能写出单元测试和写不出单元测试之间体现了编程能力上的巨大的鸿沟。不管是什么样的程序员,坚持编写一段时间的单元测试以后,都会明显感觉到代码设计能力的巨大提高。

1.2.单元测试的痛点

对于新人来讲,很容易在编写单元测试的时候遇到这几类问题:

1.2.1.单元测试的资料不够全

这里不够全是相对于“编码”来讲的。介绍如何编码、如何使用某个框架的书茫茫多,可是与编码一样重要的介绍单元测试的书却很少,翻来覆去好的也很少,而且都有必定年头了。(若是有这方面的好的资料,请推荐给我,多谢)

不少关于编程的书籍中并无深刻介绍如何进行单元测试,或者仅仅介绍了最基础的assert、jUnit里怎么定义一个测试函数之类,就没有而后了,给人的感受是这样:

book

1.2.2.单元测试难以理解和维护

测试代码不像普通的应用程序同样有着很明确的做为“值”的输入和输出。举个例子,假如一个普通的函数要作下面这件事情:

  • 接收一个user对象做为参数
  • 调用dao层的update方法更新用户属性
  • 返回true/false结果

那么,只须要在函数中声明一个参数、作一次调用、返回一个布尔值就能够了。但若是要对这个函数作一个“纯粹的”单元测试,那么它的输入和输出会有不少状况,好比其中一个测试是这样:

  • 假设调用dao层的update方法会返回true。
  • 程序去调用service层的update方法。
  • 验证一下service是否是也返回了true。

不管是用什么样的单元测试框架,最后写出来的单元测试代码量也比业务代码只多很多,我在写代码过程当中的经验值是:要在不做弊的状况下维持比较高的单元测试覆盖率,要有三倍于业务代码的单测代码。

更多的代码量,加上单测代码并不像业务代码那样直观,还有对单测代码可读性不重视的坏习惯,致使最终呈现出来的单测代码难以阅读,要维护更是难上加难。

同时,大部分单元测试的框架都有很强的代码侵入性。要理解单元测试,首先得学习他用的那个单元测试框架,这无形中又增长了单元测试理解和维护的难度。

1.2.3.单元测试难以去除依赖

就像以前说的,若是要写一个纯粹的、无依赖的单元测试每每很困难,好比依赖了数据库、或者依赖了文件系统、再或者依赖了其它模块。

因此不少人在写单元测试时选择依赖一部分资源,好比在本机启动一个数据库。这类所谓的“单元测试”每每很流行,可是对于多人合做的项目,这类测试却常常容易形成混乱。

好比说要在本地读个文件,或者链接某个数据库,其余修改代码的人(或者持续集成系统中)并无这些东西,因此测试也都无法经过。最后大部分这类测试代码的下场都是用不了、也舍不得删,只好被注释掉,扔在那里。

随着开源项目逐渐发展,对外部资源的依赖问题开始能够经过一些测试辅助工具解决,好比使用内存型数据库H2代替链接实际的测试数据库,不过能替代的资源类型始终有限。

而实际工做过程当中,还有一类难以处理的依赖问题:代码依赖。好比一个对象的方法中调用了其它对象的方法,其它对象又调用了更多对象,最后造成了一个无比巨大的调用树。

不少比较旧的描述单元测试的书里写了一些传统的办法,这类方法基本上是先对耦合的部分作模拟,再对结果部分作断言。例如能够经过继承来本身作一个假的stub对象,最终用assert的方式验证正确性。可是这至关于对于每种假设都要作一个假的对象,并且对结果进行验证也比较复杂:好比我要验证“更新”操做是否真的调用了dao层,那么要本身在stub对象里对调用进行计数,验证时再对计数进行断言,很是繁琐。

后来出现了一些mock框架,好比java的JMockit、EasyMock,或者Mockito。利用这类框架能够相对比较轻松的经过mock方式去作假设和验证,相对于以前的方式有了质的飞跃,可是即便用上这类框架,遇到复杂的业务代码每每也无能为力。

而每每新人的代码质量每每不高,尤为是对代码的拆分和逻辑的抽象还处于懵懂阶段。要对这类代码写单测,即便是工做了3,4年的高级码农也是一个挑战,对新人来讲几乎是不可能完成的任务。这也让不少新人有了“写单测很难”的感受。

因此在这里须要强调一个观点,写单元测试的难易程度跟代码的质量关系最大,而且是决定性的。项目里不管用了哪一个测试框架都不能解决代码自己难以测试的问题,因此若是你遇到的是“个人代码里依赖的东西太多了因此写不出来单测”这样的问题的话,须要去看的是如何设计和重构代码,而不是这篇文章。

1.3.推荐阅读

  • 重构-改善既有代码的设计
  • 修改代码的艺术
  • 敏捷软件开发:原则、模式与实践

2.Spock是什么

2.1.简介

这里引用官方的介绍:

Spock is a testing and specification framework for Java and Groovy applications. What makes it stand out from the crowd is its beautiful and highly expressive specification language. Thanks to its JUnit runner, Spock is compatible with most IDEs, build tools, and continuous integration servers. Spock is inspired from JUnit, jMock, RSpec, Groovy, Scala, Vulcans, and other fascinating life forms.

简单地说,spock是一个测试框架,它的核心特性有如下几个:

  • 能够应用于java或groovy应用的单元测试框架。
  • 测试代码使用基于groovy语言扩展而成的规范说明语言(specification language)。
  • 经过junit runner调用测试,兼容绝大部分junit的运行场景(ide,构建工具,持续集成等)。
  • 框架的设计思路参考了JUnit,jMock,RSpec,Groovy,Scala,Vulcans……

要理解spock的几个特性,还要理解几个关键名词:

2.1.1.groovy

引用维基百科上的介绍:

Groovy是Java平台上设计的面向对象编程语言。这门动态语言拥有相似Python、Ruby和Smalltalk中的一些特性,能够做为Java平台的脚本语言使用。

Groovy的语法与Java很是类似,以致于多数的Java代码也是正确的Groovy代码。Groovy代码动态的被编译器转换成Java字节码。因为其运行在JVM上的特性,Groovy可使用其余Java语言编写的库。

groovy是一门比较轻量,学习门槛也比较低的语言。对于只用过java语言的程序员来讲,groovy是一个很不错的开拓视野的机会。若是你没有接触过groovy,那么能够参考这两条:

  1. 能够用纯java的语法写groovy。
  2. 参考这篇快速入门

我我的比较喜欢groovy语言,在一些小项目中常用它。引用一下R大在知乎的回复

Groovy比较讨好来自Java的程序员的一点是:用它写代码能够渐进的从接近Java的风格进化为接近Ruby的风格。使用接近Java风格写Groovy时,代码几乎跟Java同样,容易上手;而学习过程当中能够逐渐用上各类相似Ruby的方便功能。

2.1.2.specification language

若是接触过不一样语言类型的开源项目的话,就会发现有些项目中找不到测试目录(test),取而代之的是一个叫“spec”的目录,好比用ruby写的项目gitlab。这里的spec实际是specification的缩写,它的背后是一种近些年来开始流行起来的编程思想:BDD(Behavior-driven development)。

关于BDD,一样是引用维基百科上的介绍:

BDD:行为驱动开发是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协做。BDD最初是由Dan North在2003年命名,它包括验收测试和客户测试驱动等的极限编程的实践,做为对测试驱动开发的回应。

BDD的作法包括:

  • 确立不一样利益相关者要实现的远景目标
  • 使用特性注入方法绘制出达到这些目标所须要的特性
  • 经过由外及内的软件开发方法,把涉及到的利益相关者融入到实现的过程当中
  • 使用例子来描述应用程序的行为或代码的每一个单元
  • 经过自动运行这些例子,提供快速反馈,进行回归测试
  • 使用“应当(should)”来描述软件的行为,以帮助阐明代码的职责,以及回答对该软件的功能性的质疑
  • 使用“确保(ensure)”来描述软件的职责,以把代码自己的效用与其余单元(element)代码带来的边际效用中区分出来。
  • 使用mock做为还未编写的相关代码模块的替身

BDD背后的编程思想超出了这篇文章的范围,这里就再也不展开。上文说的specification language其实是BDD其中一部分思想的实现手段:经过某种规范说明语言去描述程序“应该”作什么,再经过一个测试框架读取这些描述、并验证应用程序是否符合预期。

2.1.3.单元测试的运行场景

测试只有被执行以后才会有价值,这里就涉及一个“何时执行单元测试”的问题。

  1. 被接触最多的就是在IDE中执行单元测试,java程序员比较幸运,主流的java IDE均可以很好的集成了单元测试功能,单元测试代码自动生成、测试覆盖率检查等功能也都成了IDE的标配。这些功能都能让程序员在编写代码的时候直接能够运行单元测试获得反馈。ut-ide
  2. 其次,主流的构建工具(如maven、gradle)中也都实现了运行单元测试的功能,在生成二进制包以前能够对代码进行回归测试,这些构建工具均可以经过命令行调用,这是自动化构建的前提。ut-maven
  3. 在此之上,依托于构建工具提供的自动化特性,在持续集成、持续部署的过程当中能够执行自动化构建,在自动化构建的过程当中经过构建工具执行单元测试,这是持续集成的流程中的重要步骤。ut-ci

2.2.Spock与现有框架的对比

2.2.1.已有的java单元测试框架

就像刚才说的,有不少已有的单元测试框架,稍微老一点的如JMockit、EasyMock,新一点的相似Mockito和PowerMock。我以前一直在用testng+Mockito做为主要的单元测试框架,用它写过大概上万行单元测试,它的写法相对来讲比较易读,功能也能知足大多数场景。

但在使用mockito的过程当中也老是有一些不是很方便的地方,好比代码的可读性总仍是差那么一点,好比像这样:

@Test
public void testIsUserEnabled_userStatusIsClosed_returnFalse() throws Exception {

    UserInfo userInfo = new UserInfo();

    userInfo.status = UserInfo.CLOSED;

    doReturn(userInfo).when(userDao).getUserInfo(anyLong());

    boolean isUserEnabled = userService.isUserEnabled(1l);

    Assert.assertFalse(isUserEnabled);

}


虽然能读懂,可是对于它所作的事情全来讲感受说了不少废话,单元测试代码老是里充斥着各类when(),anyXXX(),return()之类啰嗦的关键词,加上java自己就是一个啰嗦的强类型的语言,这让写单测和读单测成为了一种体力活。

其次是单测数据,大部分测试都要提供数据,好比“当输入a的时候应该返回b”,若是只有一组数据那么没什么问题,可是当须要测试不少边界条件,须要多组数据的时候就会比较纠结。

用jUnit或者testng的dataprovider能够实现这个需求,可是不管是经过xml定义仍是经过函数返回数据,都很是不方便。

最后,由于这些框架都只是一些独立的函数,没有告诉你“应该怎么写单测”,因此不一样的人最终写出来的单测也是五花八门:

  • 有不用assert而是用system.out.println的
  • 有单测一个函数写了好几百行的
  • 有直接把单测当成main函数写的

最终,团队要接受“虽然确实写了单测,然而这并无什么卵用”的结果。

2.2.2.为何使用spock

仍是刚才的例子,若是用spock写的话:

def "isUserEnabled should return true only if user status is enabled"() {
    given:
    UserInfo userInfo = new UserInfo(
        status: actualUserStatus
    );

    userDao.getUserInfo(_) >> userInfo;

    expect:

    userService.isUserEnabled(1l) == expectedEnabled;

    where:

    actualUserStatus   | expectedEnabled

    UserInfo.ENABLED   | true

    UserInfo.INIT     | false

    UserInfo.CLOSED    | false

}


这段代码实际是3个测试:当getUserInfo返回的用户状态分别为ENABLED、INIT和CLOSED时,验证各自isUserEnabled函数的返回是否符合期待。

我对于spock框架最直接的感觉:

  • spock框架使用标签分隔单元测试中不一样的代码,更加规范,也符合实际写单元测试的思路
  • 代码写起来更简洁、优雅、易于理解
  • 因为使用groovy语言,因此也能够享受到脚本语言带来的便利
  • 底层基于jUnit,不须要额外的运行框架
  • 已经发布了1.0版本,基本没有比较严重的bug

2.2.3.为何不用spock

用了一段时间的spock后,我也总结了几个不用spock的理由:

  • 框架相对比较新,IDE的支持(尤为是eclipse)不如其它成熟的框架
  • groovy语言自己的compiler更新比较快,偶尔有坑(版本不兼容等)
  • 须要了解groovy语言
  • 与其它java的测试框架风格相差比较大,须要适应

固然,这些理由比起spock提供的易于开发和维护的单元测试代码来讲,都是能够忽略的。

3.使用Spock

写到这里,仍是要聚焦一下这篇文章要讨论的问题:如何用spock框架编写单元测试,在此以前再强调一下:

  • 单元测试不必定非要使用spock,可是其它框架写出的单元测试代码远没有用spock框架优雅。
  • spock框架并不仅能写单元测试,它也能够写集成测试,甚至性能测试,可是后二者spock相对于其它框架来讲没有什么优点。

3.1.关于开发环境

在使用spock框架时,我比较推荐的ide是IDEA,推荐的构建工具是gradle。

就算不使用spock框架,IDEA的顺手程度也比eclipse好太多,对新技术的响应速度快,也没有那么多莫名其妙的严重bug,社区版免费但主要功能都有,没有什么理由不试用一下。

而gradle相对于maven来讲配置简化了不少,可定制的功能也更强,与其迷失在maven复杂的xml和一层套一层的依赖关系中,我宁愿把时间作一些更有意思的事情。

因为IDE基本能够自由选择,但构建工具大部分是由团队决定的,而maven如今仍是处于构建工具的领导地位,因此这篇文章里的步骤都是基于IDEA+maven,当前的IDEA已经支持spock,不须要作什么特殊配置。

  • 若是你的团队应用了gradle,spock官网中对于gradle如何配置说的比较完整,能够直接参考官网。
  • 若是你执迷不悟非要使用eclipse,我在eclipse下也跑通了整个流程。须要安装最新的groovy-eclipse插件和附加包(安装时选择groovy2.4版以上的compiler),地址:https://github.com/groovy/groovy-eclipse/wiki

3.2.hello spock

前面作了那么多铺垫,终于到了真正编写一个hello world的时候。

到这里,我假设你是一位java开发者,而且已经了解基本的IDE及构建工具的使用。

  1. 建立一个空白项目:hello_spock,选择maven工程。
  2. 在pom.xml中增长依赖:
    <?xml version="1.0" encoding="UTF-8"?>
    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>hello</groupId>
        <artifactId>hello_spock</artifactId>
        <version>1.0-SNAPSHOT</version>
        <dependencies>
            <!-- Mandatory dependencies for using Spock -->
            <dependency>
                <groupId>org.spockframework</groupId>
                <artifactId>spock-core</artifactId>
                <version>1.0-groovy-2.4</version>
                <scope>test</scope>
            </dependency>
    
            <!-- Optional dependencies for using Spock -->
            <dependency> 
                <!-- use a specific Groovy version rather than the one specified by spock-core -->
                <groupId>org.codehaus.groovy</groupId>
                <artifactId>groovy-all</artifactId>
                <version>2.4.3</version>
            </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>
    
            <dependency>
                <!-- enables mocking of classes without default constructor (together with CGLIB) -->
                <groupId>org.objenesis</groupId>
                <artifactId>objenesis</artifactId>
                <version>2.1</version>
                <scope>test</scope>  
            </dependency>
        </dependencies>
    </project>

     

  3. 因为spock是基于groovy语言的,因此须要建立groovy的测试源码目录:首先在test目录下建立名为groovy的目录,以后将它设为测试源码目录。ide-1
  4. 建立一个简单的类:
    public class Sum {
    
        public int sum(int first, int second) {
    
            return first + second;
    
        }
    }

     

  5. 建立测试类,能够手工建立,也可使用IDEA的辅助建立:ide-2

    ide-3

    ide-4

  6. 编写测试代码,这里咱们验证一下sum返回的结果是否正确:
    import spock.lang.Specification
    
    class SumTest extends Specification {
    
        def sum = new Sum();
        def "sum should return param1+param2"() {
            expect:
            sum.sum(1,1) == 2
        }  
    
    }

     

  7. 运行一下测试:ide-5

至此,一个最简单的spock测试就写完了。

3.3.Spock中的概念

3.3.1.Specification

在Spock中,待测系统(system under test; SUT) 的行为是由规格(specification) 所定义的。在使用Spock框架编写测试时,测试类须要继承自Specification类。

3.3.2.Fields

Specification类中能够定义字段,这些字段在运行每一个测试方法前会被从新初始化,跟放在setup()里是一个效果。

def obj = new ClassUnderSpecification()

def coll = new Collaborator()


3.3.3.Fixture Methods

预先定义的几个固定的函数,与junit或testng中相似,很少解释了

def setup() {}          // run before every feature method

def cleanup() {}        // run after every feature method

def setupSpec() {}     // run before the first feature method

def cleanupSpec() {}   // run after the last feature method


3.3.4.Feature methods

这是Spock规格(Specification)的核心,其描述了SUT应具有的各项行为。每一个Specification都会包含一组相关的Feature methods,如要测试1+1是否等于2,能够编写一个函数:

def "sum should return param1+param2"() {

    expect:

    sum.sum(1,1) == 2

}


3.3.5.blocks

每一个feature method又被划分为不一样的block,不一样的block处于测试执行的不一样阶段,在测试运行时,各个block按照不一样的顺序和规则被执行,以下图:

blocks

下面分别解释一下各个block的用途。

3.3.6.Setup Blocks

setup也能够写成given,在这个block中会放置与这个测试函数相关的初始化程序,如:

setup:

def stack = new Stack()

def elem = "push me"


通常会在这个block中定义局部变量,定义mock函数等。

3.3.7.When and Then Blocks

when与then须要搭配使用,在when中执行待测试的函数,在then中判断是否符合预期,如:

when:

stack.push(elem)  

then:

!stack.empty

stack.size() == 1

stack.peek() == elem


3.3.7.1.断言

条件相似junit中的assert,就像上面的例子,在then或expect中会默认assert全部返回值是boolean型的顶级语句。若是要在其它地方增长断言,须要显式增长assert关键字,如:

def setup() {

  stack = new Stack()

  assert stack.empty

}


3.3.7.2.异常断言

若是要验证有没有抛出异常,能够用thrown(),以下:

when:

stack.pop()  

then:

thrown(EmptyStackException)

stack.empty


要获取抛出的异常对象,能够用如下语法:

when:

stack.pop()  

then:

def e = thrown(EmptyStackException)

e.cause == null


若是要验证没有抛出某种异常,能够用notThrown():

def "HashMap accepts null key"() {

  setup:

  def map = new HashMap()  

  when:

  map.put(null, "elem")  

  then:

  notThrown(NullPointerException)

}


3.3.8.Expect Blocks

expect能够看作精简版的when+then,如:

when:

def x = Math.max(1, 2)  

then:

x == 2


能够简化为:

expect:

Math.max(1, 2) == 2


3.3.9.Cleanup Blocks

函数退出前作一些清理工做,如关闭资源等。

3.3.10.Where Blocks

作测试时最复杂的事情之一就是准备测试数据,尤为是要测试边界条件、测试异常分支等,这些都须要在测试以前规划好数据。可是传统的测试框架很难轻松的制造数据,要么依赖反复调用,要么用xml或者data provider函数之类难以理解和阅读的方式。好比说:

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

    }
}


而在spock中,经过where block可让这类需求实现起来变得很是优雅:

class DataDriven extends Specification {

    def "maximum of two numbers"() {

        expect:

        Math.max(a, b) == c

        where:

        a | b || c

        3 | 5 || 5

        7 | 0 || 7

        0 | 0 || 0

    }
}


上述例子实际会跑三次测试,至关于在for循环中执行三次测试,a/b/c的值分别为3/5/5,7/0/7和0/0/0。若是在方法前声明@Unroll,则会当成三个方法运行。

更进一步,能够为标记@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

    }
}


运行时,名称会被替换为实际的参数值。

除此以外,where block还有两种数据定义的方法,而且能够结合使用,如:

where:

a | _

3 | _

7 | _

0 | _


b << [5, 0, 0]

c = a > b ? a : b


3.4.Interaction Based Testing

对于测试来讲,除了可以对输入-输出进行验证以外,还但愿能验证模块与其余模块之间的交互是否正确,好比“是否正确调用了某个某个对象中的函数”;或者指望被调用的模块有某个返回值,等等。

各种mock框架让这类验证变得可行,而spock除了支持这类验证,而且作的更加优雅。若是你还不清楚mock是什么,最好先去简单了解一下,网上的资料很是多,这里就不展开了。

3.4.1.mock

在spock中建立一个mock对象很是简单:

class PublisherSpec extends Specification {

    Publisher publisher = new Publisher()

    Subscriber subscriber = Mock()

    Subscriber subscriber2 = Mock()

    def setup() {

        publisher.subscribers.add(subscriber)

        publisher.subscribers.add(subscriber2)

    }
}


而建立了mock对象以后就能够对它的交互作验证了:

def "should send messages to all subscribers"() {

    when:

    publisher.send("hello")

    then:

    1 * subscriber.receive("hello")

    1 * subscriber2.receive("hello")
}


上面的例子里验证了:在publisher调用send时,两个subscriber都应该被调用一次receive(“hello”)。

示例中,表达式中的次数、对象、函数和参数部分均可以灵活定义:

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")     // 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({ it.size() > 3 }) // an argument that satisfies the given predicate

                                          // (here: message length is greater than 3)

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


得益于groovy脚本语言的特性,在定义交互的时候不须要对每一个参数指定类型,若是用过java下的其它mock框架应该会被这个特性深深的吸引住。

3.4.2.Stubbing

对mock对象定义函数的返回值能够用以下方法:

subscriber.receive(_) >> "ok"

 


符号表明函数的返回值,执行上面的代码后,再调用subscriber.receice方法将返回ok。若是要每次调用返回不一样结果,可使用:

 

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


若是要作额外的操做,如抛出异常,可使用:

subscriber.receive(_) >> { throw new InternalError("ouch") }


而若是要每次调用都有不一样的结果,能够把屡次的返回链接起来:

subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"


3.5.mock and stubbing

若是既要判断某个mock对象的交互,又但愿它返回值的话,能够结合mock和stub,能够这样:

then:

1 * subscriber.receive("message1") >> "ok"

1 * subscriber.receive("message2") >> "fail"


注意,spock不支持两次分别设定调用和返回值,若是把上例写成这样是错的:

setup:

subscriber.receive("message1") >> "ok"

when:

publisher.send("message1")

then:

1 * subscriber.receive("message1")


此时spock会对subscriber执行两次设定:

  • 第一次设定receive(“message1”)只能调用一次,返回值为默认值(null)。
  • 第二次设定receive(“message1”)会返回ok,不限制次数。

3.6.其它类型的mock对象

spock也支持spy,stub之类的mock对象,可是并不推荐使用。由于使用“正规的”bdd思路写出的代码不须要用这些方法来测试,官方的解释是:

Think twice before using this feature. It might be better to change the design of the code under specification

具体的使用方法若是有兴趣能够参考官方文档。

3.7.更多

至此,读者应该对Spock的主要功能和使用方法应该有个粗略的认识。若是但愿实际使用spock,推荐读一下官方的文档,写的比较清晰,而且其中引用的一些文档也都值得一读:

http://spockframework.github.io/spock/docs/1.0/index.html

另一个值得一看的是spock-example工程:

https://github.com/spockframework/spock-example

4.结语

须要再强调一下:现实中的场景绝对会比文章中的例子复杂(好比要mock一个private函数,或者全局变量,或者静态函数,等等),可是此时更好的思路并非压榨框架的功能,而应该是去思考代码的设计是否出了问题。

仍是强调这个观点:单元测试的难度和代码设计的好坏息息相关,单元测试测的三分是代码,七分是设计。若是你以为本身处于编码能力上升的瓶颈期,那么能够尝试一下为之前写的类编写“纯粹的”单元测试,在这个过程当中,spock可让你从重复的编码、繁重的维护工做中解脱出来,让编写测试回归为一件有幸福感和成就感的事情。

spock

相关文章
相关标签/搜索