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

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