1.Spock是什么

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应用的测试框架,框架设计参考了JUnit,jMock,RSpec,Groovy,Scala,Vulcans等测试框架,能够兼容绝大部分Junit的运行环境(IDE,构建工具,持续集成等),且能够很好的与Spring、Guice、Tapestry、Unitils及Grails等框架集成。

Spock的测试代码是采用Groovy代码进行测试,能够很好的利用Groovy语言的特性,关于Groovy可以参考Groovy for Java developer: learning Groovy-specific features

2. Spock的基本概念

2.1 Demo

先来写一个简单的示例代码:

import spock.lang.Specification

class DemoTest extends Specification {
    def sum = new Demo();
    def "Sum"() {
        expect:
        sum.sum(1,1) == 2
    }
}

2.2 Specification

Specification来自BDD(行为驱动开发) 概念,通过某种规范说明语言去描述程序“应该”做什么,再通过一个测试框架读取这些描述、并验证应用程序是否符合预期,详细关于BDD的思想参见BDD wiki .

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

2.3 Fields

Specification类中可以定义字段,这些字段在运行每个测试方法前会被重新初始化,跟放在setup()里是一个效果,如在demo测试中的

 def sum = new Demo()

但Fileds与Java中的属性有所不同,Filed并不是被各个测试方法(feature method)共享的,每个测试方法都会有一个独立的fileds,这更加符合测试的要求。但在一些特殊的情况下,需要测试方法之间共享变量,这时候只需要使用 @Shared 即可。

@Shared sum = new Demo()

2.4 Fixture Methods (Spock预定义方法)

Spock主要有以下自定义方法:

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

这些方法与Junit基本类似。

2.5 Feature Methods(测试方法)

类似与Junit中的测试方法,主要描述待侧系统的各项行为。理论上一个Feature method包含了四个阶段:

  • Set up(基本设置)
  • Stimulus(执行语句)
  • Response(测试结果)
  • Cleanup(清理)

其中Set up和 Cleanup是可选的。

2.6 blocks

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

  • setup

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

 def "Sum"() {
        setup:
        def sum = new Demo();
        expect:
        sum.sum(1,1) == 2
    }
  • when … then ...

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

class BasicInfoServiceImplTest extends Specification {
    def toTestObj = new BasicInfoServiceImpl();

    def "GetMobile"() {
        when:

        String mobile = toTestObj.getMobile(1);
        then:

        !mobile.isEmpty();
        mobile.equals("11111111111")
    }
}
  • expert

条件类似junit中的assert,就像上面的例子,在then或expect中会默认assert所有返回值是boolean型的顶级语句。如果要在其它地方增加断言,需要显式增加assert关键字,如:

    def "getMobileExpect"(){
        expect:
        toTestObj.getMobile(2).equals("22222222222")
    }

如果要验证有没有抛出异常,可以用thrown(),如下:

def "getMobileAssert"(){
        when:
        String mobile = toTestObj.getMobile(4);
        then:
        assert mobile == null;

        when:
        toTestObj.getMobile(3);
        then:
        thrown(IllegalArgumentException)
    }

如果要验证没有抛出某种异常,可以用notThrown()。

  • clearup

    主要做一些清理工作,例如关闭资源连接等。

  • where

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

def "getMobileWhere"(){
        expect:
        toTestObj.getMobile(userId) == expeted

        where:
        userId||expeted
        1||"11111111111"
        2||"22222222222"
        4||null
    }

3. 使用Spock进行mock测试

对于测试来说,除了能够对输入-输出进行验证之外,还希望能验证模块与其他模块之间的交互是否正确,比如“是否正确调用了某个某个对象中的函数”;或者期望被调用的模块有某个返回值,等等。

    UserInfoServiceImpl toBeTest = new UserInfoServiceImpl();
    BasicInfoService mockBasic = Mock();
    UserInfoServiceImpl mockedUserService = Mock();

    def setup(){
        toBeTest.setBasicInfoService(mockBasic);
    }
    def "getUserWithBasicMock"() {
        setup:
        mockBasic.getMobile(1)>>"23234355"
        when:
        User user= toBeTest.getUser(1);
        then:
        user != null
        user.getMobile() != null
        user.getMobile().equals("23234355")

    }

通过简单的Mock()即可生成 mock object ,通过>>的方式可以设定对应方法模拟返回值。

        mockBasic.getMobile(_)>>"23234355"

如果需要其他操作,如抛出错误:

        mockBasic.getMobile(_)>>{throw new IllegalArgumentException()}

如果需要每次操作都有不同的返回值可以如下操作:

        mockBasic.getMobile(_)>>>["1mobile","2mobile","3mobile"]

3.1 Stub

stub 可以理解为测试桩,它能实现当特定的方法被调用时,返回一个指定的模拟值。如果你的测试用例需要一个伴生对象来提供一些数据,可以使用 stub 来取代数据源,在测试设置时可以指定返回每次一致的模拟数据。

mock 通常需要你事先设定期望。你告诉它你期望发生什么,然后执行测试代码并验证最后的结果与事先定义的期望是否一致。简单说就是你可以用stub伪造(fade)一个方法,阻断对原来方法的调用,它是stub是因为它也可以像stub一样伪造方法,阻断对原来方法的调用, expectation是说它不仅伪造了这个方法,它还期望你(必须)调用这个方法,如果没有被调用到,这个testfail了。

Stub方法除了具有Mock的类似方法,还可以将多个返回值连接起来:

        mockBasic.getMobile(_)>>>["1mobile","2mobile","3mobile"]>>{throw new IllegalArgumentException()}>>>["5mobile","6mobile"]

如:

    def "getUserWithBasicMockMultiResult"() {
        setup:
        mockBasic = Stub()
        toBeTest.setBasicInfoService(mockBasic)
        mockBasic.getMobile(_)>>>["1mobile","2mobile"] >>"3mobile"

        when:
        User user= toBeTest.getUser(1);
        then:
        user != null
        user.getMobile() != null
        user.getMobile().equals("1mobile")

        when:
        user = toBeTest.getUser(1)
        then:
        user.getMobile().equals("2mobile")

        when:
        user = toBeTest.getUser(1)
        then:
        user.getMobile().equals("3mobile")

    }

4. 在Java项目中使用

4.1 与Maven集成

阐述完了Spock的基本概念,如果想要在Java的生产项目中使用,还要解决持续集成工具(如jekins)等的兼容问题,与这些工具的集成本质上是对Maven的集成。

因为Spock是使用Groovy语言来测试,因此test代码需要在test目录下新建groovy 文件夹,并将其作为ut的根目录。同时在pom中进行配置如下依赖:

<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>

并使用maven-surefire-plugin指定test的文件:

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18</version>
                <configuration>
                    <includes>
                        <include>**/*Test.java</include>
                        <include>**/*Spec.java</include>
                    </includes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <compilerId>groovy-eclipse-compiler</compilerId>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.codehaus.groovy</groupId>
                        <artifactId>groovy-eclipse-compiler</artifactId>
                        <version>2.8.0-01</version>
                    </dependency>
                    <dependency>
                        <groupId>org.codehaus.groovy</groupId>
                        <artifactId>groovy-eclipse-batch</artifactId>
                        <version>2.1.8-01</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>

4.2 与Spring框架集成

现在IOC框架已经开始成为Java项目的标配,在这里简单介绍如何与Spring框架进行集成。

在pom中新增Spock-spring的依赖:

        <dependency>
            <groupId>org.spockframework</groupId>
            <artifactId>spock-spring</artifactId>
            <version>1.0-groovy-2.4</version>
        </dependency>

在测试class中配置spring的配置文件即可:

@ContextConfiguration(locations = 'classpath*:/config/appcontext-*')

如:

@ContextConfiguration(locations = 'classpath*:/config/appcontext-*')
class UserServiceSpockTestWithSpring extends Specification{
    @Resource
    UserService userService;

    def "getUserWithSpring" (){
        when:
        User user = userService.getUser(1)
        then:
        user != null
        user.getMobile().equals("11111111111")
    }

}

5. 现有测试框架的对比:Junit,Mockito等

详情可以参考:Spock versus JUnit

在最后本文所使用到的示例代码可以查看:java-spock-demo

References

使用Spock框架进行单元测试

Using Spock to test Groovy AND Java applications 

Flexibly ignore tests in Spock

Spock primer

Mocks, Mockito, and Spock

在Spring Boot项目中使用Spock框架

Spock versus JUnit

Spock VS Mockito

理解测试中的stub和mock