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

相關文章
相關標籤/搜索