SpringBoot+Spock的熟悉之路(二):Spock,Mock和Mockitio的关系

前言

本来打算是把Spock的使用写成一篇的,后来发现太长了而且结构比较冗杂,还是拆分出来比较好,而且这样也比较好检索。初次使用,如果有误,请轻喷,并指出问题。我会及时纠正。
要使用Spock,必须要有Junit和Mock的基本概念和使用。建议在使用Spock之前先懂了解一下Junit和Mockitio
上一篇博客:《SpringBoot+Spock的熟悉之路(一):能正常启动并使用Spock
下一篇博客:《SpringBoot+Spock的熟悉之路(三):用Spock对SpringBoot进行单元测试》

环境

一定要注意版本的问题!一定要注意版本的问题!一定要注意版本的问题!

Tool Version
Intellij IDEA 2018.3 Ultimate
SpringBoot 2.0.1
Java 1.8
mybatis-spring-boot 2.0.1
Groovy 2.4.6

依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.0.1</version>
    </dependency>
    <!--服务器配置的是Oracle-->
    <dependency>
        <groupId>com.oracle</groupId>
        <artifactId>ojdbc8</artifactId>
        <version>12.2.0.1</version>
    </dependency>
        
    <!---------------Spock必须的------------------->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
	    <groupId>org.spockframework</groupId>
	    <artifactId>spock-core</artifactId>
	    <version>1.3-groovy-2.4</version>
	    <scope>test</scope>
	</dependency>
	<dependency>
	    <groupId>org.spockframework</groupId>
	    <artifactId>spock-spring</artifactId>
	    <version>1.3-RC1-groovy-2.4</version>
	    <scope>test</scope>
	</dependency>
	<dependency>
	    <groupId>org.codehaus.groovy</groupId>
	    <artifactId>groovy-all</artifactId>
	    <version>2.4.6</version>
	</dependency>
	<!--------------  可选项  ----------------->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
	    <groupId>org.projectlombok</groupId>
	    <artifactId>lombok</artifactId>
	    <optional>true</optional>
    </dependency>
    <dependency> <!-- 允许MockInterface等 -->
	   <groupId>net.bytebuddy</groupId>
	   <artifactId>byte-buddy</artifactId>
	   <version>1.9.3</version>
	   <scope>test</scope>
	</dependency>
	<dependency>
	   <groupId>org.objenesis</groupId>
	   <artifactId>objenesis</artifactId>
	   <version>2.6</version>
	   <scope>test</scope>
	</dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

Spock和Mockito

本篇并不会着重讲Mockito的基本概念和所有用法,想了解请参考专门讲解Mockito的博客

Mockito的简述

我们代码有时候不可避免的会与其他模块或者其他层产生依赖,比如我们的Controller里一般会自动@Autowired一个Service,而我们的项目一般还有BaseController,BaseController里还会@Autowired一些我们自己的工具类比如ExcelUtils等,这些东西不一定会在我们所有的Controller里面都用到,我们也不想在每个Controller测试里都去处理BaseController中的依赖问题。为了解决这种情景,Mock应运而生,而Mockito只是java使用Mock的其中一个框架。

Mock,Stub和Spy

注意,这一个模块下面的写法为Mockitio的,不是Spock的
这三个是Mockito框架中的三种基本行为

数据准备

package com.example.demo.entity;

import lombok.Data;

@Data
public class DemoEntity {

    private Integer number;

    private String str;

    public int method1(){
        return 1;
    }

    public String method2(){
        return "hello";
    }
}

Mock

作用为可以概括为把被Mock的对象里的成员变量以及设为Null或者0,并将方法设为空的方法体

package com.example.demo.simple

import com.example.demo.entity.DemoEntity
import org.mockito.Mockito
import spock.lang.Shared
import spock.lang.Specification


class MockSpec extends Specification {

    @Shared
    DemoEntity demoEntity

    def setup(){
    }

    def "mock test"(){
        given:
        demoEntity = Mockito.mock(DemoEntity.class)
        expect:
        demoEntity.method1() == 0
        demoEntity.str == null
    }

}

执行后是在这里插入图片描述
可以看出来,Mock会将一个类里的需要赋值的变量或者方法赋为0或者null

Stub

当我们需要被mock对象里的某些方法,并且希望这些方法以我们期望的值进行返回时,可以对这些方法进行stub,来规定它的返回值。也可以理解为重新定义一个方法的返回值。

def "stub test"(){
   given:
    demoEntity = Mockito.mock(DemoEntity.class)
    //Stub往往和Mock一起行动。相当于在这里定义method1的行为
    //这行代码的意思是,命令demoEntity的method1方法被调用时返回5。这种重新定义的行为叫做stub
    Mockito.when(demoEntity.method1()).thenReturn(5)
    expect:
    demoEntity.method1() == 5
    demoEntity.method2() == null
}

执行后是在这里插入图片描述
可以看出来原来的方法已经被Stub过后的行为覆盖,而没有进行定义的方法依旧为空

Spy

不同于Mock和Stub可以在interface和abstract class起作用,Spy只能作用在一个实体类
概括起来就是,Spy会覆盖被Stub的方法而保留没有被stub的实际的方法。

def "spy test"(){
    given:
    demoEntity = Mockito.spy(DemoEntity.class)
    Mockito.when(demoEntity.method1()).thenReturn(5)
    expect:
    demoEntity.method1() == 5
    demoEntity.method2() == "hello"
}

执行过后,在这里插入图片描述
可以看出来,没有被Stub的方法使用的是原来自己定义的行为

Spock中的Mock,Stub和Spy

Spock本身就支持三种基本行为,并且有自己的写法

DemoEntity demoEntity1 = Mock()  //或者 def classa = Mock(ClassA)

DemoEntity demoEntity2 = Stub() // 或者 def classb = Stub(ClassB)

DemoEntity demoEntity3 = Spy() //或者 def classc = Spy(ClassC)

当然也可以定义和stub混合的写,例如

DemoEntity demoEntity1 = Mock(){
	demoEntity1 .method1() >> 5
	//这种写法是Spock的语法优势之一
	//这里可以理解为,规定classa里的method1方法被调用后返回一个叫“hello”的字符串
}

如果想同时享受groovy的闭包优势和编辑器的代码提示,也可以这么写

DemoEntity demoEntity1 = Mock(DemoEntity ){
	method1() >> 5
}

那么Mock()对象和Stub()对象有什么区别呢?
按照Spock官方的说法,Stub()对象不关心method被执行了多少次,只关心返回值。这么说有点晦涩难懂,我举个例子

class SpockMockSpec extends Specification{
	DemoEntity demoEntity1 = Mock(DemoEntity ){
		method1() >> 5
	}

	def"number of call test"(){
		given:
		//TODO:给一些初始值
		when:
		//TODO:执行一些操作,让demoEntity1的method1方法被调用两次
		demoEntity1.method1()
		demoEntity1.method1()
		then:
		//Spock语法,意思是判断classa中的method1方法是否被调用了两侧
		2 * demoEntity1.method1()
	}	
}

如果是这么写,那么这个测试是通过的,因为我们确实在程序中调用了两次classa.method1()
但是如果将Mock那块的写法换成了

DemoEntity demoEntity1 = Stub(DemoEntity){
	method1() >> 5
}

在执行后,控制台会报错

org.spockframework.runtime.InvalidSpecException: Stub 'demoEntity1' matches the following required interaction:
2 * demoEntity1.method1()   (0 invocations)
Remove the cardinality (e.g. '1 *'), or turn the stub into a mock.

意思很明显,用Stub定义的对象无法进行调用次数判断,如果要使用该功能请将stub换成mock

尽量不要混合使用

在Spock中可以用将自己的Mock()方法和Mockito的@MockBean注解混合使用。但我个人建议请谨慎使用。在使用Mocktio的注解之前,一定要先查一下Spock是否支持Mockito的这个注解,一定要清晰的认识一点:
Spock不是Mocktio。比如处理依赖mock的时候,Mocktio有专门的@InjectMocks和@Mock组合,而Spock并不支持该写法,需要进入第三方组件。这个在之后写单元测试的博客的时候会做到讲解

部分语法对比

Spock完整语法请看官方文档中的3,4,5,6部分。其中第5部分与Mockitio的对比比较多

Spock Mockito
Mock() @MockBean
Stub() @StubBean
Spy() @SpyBean
classa.method1(_ as List) >> “5” Mockito.when(classa.method2(Mockito.anyList())).thenReturn(“5”)
list.size() == 2 Assert.equals(list.size(),2)
2 * list.add(“a”) Mockito.verify(list, Mockito.times(2)).add(“a”);
(1…_) * list.add(“a”) Mockito.verify(list, Mockito.atLeastOnce()).add(“a”);
@Subject and @Collaborator @InjectMocks and @Mock

遇到的问题

本模块主要是把自己踩过的一些比较大的坑给单独列举出来

@InjectMocks在Spock中失效的问题

详细代码可以看我的下一篇博客。Spock并不支持@InjectMocks和@Mock的组合,要使用对应的功能可以引入Mockitio为Spock专门开发的第三方工具。
总之一句话,一定要搞清楚,虽然Spock中可以使用Mockitio的功能,但Spock终究不是Mockitio,在使用Mockitio的注解之前一定要搞清楚Spock是否支持此功能

参考资料

Spock in Java 慢慢爱上写单元测试
spock-testing-exceptions-with-data-tables
Spock官方文档
Spock开源GitHub
Difference between Mock / Stub / Spy in Spock test framework
Mocks Aren’t Stubs
@Mock/@InjectMocks for groovy - spock

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章