spock單元測試框架入門知識

使用Spock框架進行單元測試

1. 摘要

最近一段時間接觸到了spock這個可以用於java和groovy項目的單元測試框架,寫了一段時間單測之後認爲這個框架不錯,值得寫一篇文章推廣一下。

2. 關於單元測試

很多人一談到單元測試就會想到xUnit框架。對於一些java新人來說,會用jUnit就是會寫單元測試,高級點的會搗鼓一下testng,然後就認爲自己掌握了單元測試。

而實際上,很多人不怎麼會寫單元測試,甚至不知道單元測試究竟是幹什麼的。寫單元測試要比寫代碼要難上許多,而這裏說的難度跟框架沒什麼關係。

所以,在開始介紹spock之前,需要先拋開框架,談談單元測試本身的事情。在理解了單元測試之後才能更清楚spock框架是什麼,以及它否能夠更優雅的解決你的問題。

2.1.1. 單元測試是什麼

寫代碼免不了要做測試,測試有很多種,對於java來說,最初級的就是寫個main函數運行一下看看結果,高級的可以用各種高大上的複雜的測試系統。每種測試都有它的關注點,比如測試功能是不是正確,或者運行狀態穩不穩定,或者能承受多少負載壓力,等等。

那麼所謂的單元測試是什麼?這裏直接引用維基百科上的詞條說明:

單元測試(又稱爲模塊測試, Unit Testing)是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數、過程等;對於面向對象編程,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。

所以,我眼中的“合格的”單元測試需要滿足幾個條件:

  1. 測試的是一個代碼單元內部的邏輯,而不是各模塊之間的交互。
  2. 無依賴,不需要實際運行環境就可以測試代碼。
  3. 運行效率高,可以隨時執行。

2.1.2. 單元測試的定位

瞭解了單元測試是什麼之後,第二個問題就是:單元測試是用來做什麼的?

很多人第一反應是“看看程序有沒有問題”,或者“確保沒有bug”。單元測試確實可以測試程序有沒有問題,但是,從我個人編程的經驗來看,大部分情況下只是使用單元測試來“看看程序有沒有問題”的話,效率反而不如把程序運行起來直接查看結果。原因有兩個:

  1. 單元測試要寫額外的代碼,而不寫單元測試,直接運行程序也可以測試程序有沒有問題。
  2. 即使通過了單元測試,程序在實際運行的時候仍然有可能出問題。

但是,很多時候直接啓動程序測試會比較慢,所以一些同學爲了解決這個問題,採用了一個折中的辦法:只加載要測試的模塊和它所有的依賴模塊,比如在測試時只加載這個模塊相關的spring的配置文件。這時所謂的單元測試實際上是用xUnit框架運行的集成測試,並沒有體現“單元”的概念。

而關於“純粹的單元測試”在介紹語言或者框架的書裏很少被提起,反而是介紹重構或者敏捷開發的書裏經常會看到各種各樣的關於單元測試的介紹。

在這裏我總結了一下幾個比較常見的單元測試的幾個典型場景:

  1. 開發前寫單元測試,通過測試描述需求,由測試驅動開發。
  2. 在開發過程中及時得到反饋,提前發現問題。
  3. 應用於自動化構建或持續集成流程,對每次代碼修改做迴歸測試。
  4. 作爲重構的基礎,驗證重構是否可靠。

還有最重要的一點:編寫單元測試的難易程度能夠直接反應出代碼的設計水平,能寫出單元測試和寫不出單元測試之間體現了編程能力上的巨大的鴻溝。無論是什麼樣的程序員,堅持編寫一段時間的單元測試之後,都會明顯感受到代碼設計能力的巨大提升。

2.2. 單元測試的痛點

對於新人來說,很容易在編寫單元測試的時候遇到這幾類問題:

2.2.1. 單元測試的資料不夠全

這裏不夠全是相對於“編碼”來說的。介紹如何編碼、如何使用某個框架的書茫茫多,但是與編碼同樣重要的介紹單元測試的書卻不多,翻來覆去好的也不多,並且都有一定年頭了。(如果有這方面的好的資料,請推薦給我,多謝)

很多關於編程的書籍中並沒有深入介紹如何進行單元測試,或者僅僅介紹了最基礎的assert、jUnit裏怎麼定義一個測試函數之類,就沒有然後了,給人的感覺是這樣:

2.2.2. 單元測試難以理解和維護

測試代碼不像普通的應用程序一樣有着很明確的作爲“值”的輸入和輸出。舉個例子,假如一個普通的函數要做下面這件事情:

  • 接收一個user對象作爲參數
  • 調用dao層的update方法更新用戶屬性
  • 返回true/false結果

那麼,只需要在函數中聲明一個參數、做一次調用、返回一個布爾值就可以了。但如果要對這個函數做一個“純粹的”單元測試,那麼它的輸入和輸出會有很多情況,比如其中一個測試是這樣:

  • 假設調用dao層的update方法會返回true。
  • 程序去調用service層的update方法。
  • 驗證一下service是不是也返回了true。

無論是用什麼樣的單元測試框架,最後寫出來的單元測試代碼量也比業務代碼只多不少,我在寫代碼過程中的經驗值是:要在不作弊的情況下維持比較高的單元測試覆蓋率,要有三倍於業務代碼的單測代碼。

更多的代碼量,加上單測代碼並不像業務代碼那樣直觀,還有對單測代碼可讀性不重視的壞習慣,導致最終呈現出來的單測代碼難以閱讀,要維護更是難上加難。

同時,大部分單元測試的框架都有很強的代碼侵入性。要理解單元測試,首先得學習他用的那個單元測試框架,這無形中又增加了單元測試理解和維護的難度。

2.2.3. 單元測試難以去除依賴

就像之前說的,如果要寫一個純粹的、無依賴的單元測試往往很困難,比如依賴了數據庫、或者依賴了文件系統、再或者依賴了其它模塊。

所以很多人在寫單元測試時選擇依賴一部分資源,比如在本機啓動一個數據庫。這類所謂的“單元測試”往往很流行,但是對於多人合作的項目,這類測試卻經常容易造成混亂。

比如說要在本地讀個文件,或者連接某個數據庫,其他修改代碼的人(或者持續集成系統中)並沒有這些東西,所以測試也都沒法通過。最後大部分這類測試代碼的下場都是用不了、也捨不得刪,只好被註釋掉,扔在那裏。

隨着開源項目逐漸發展,對外部資源的依賴問題開始可以通過一些測試輔助工具解決,比如使用內存型數據庫H2代替連接實際的測試數據庫,不過能替代的資源類型始終有限。

而實際工作過程中,還有一類難以處理的依賴問題:代碼依賴。比如一個對象的方法中調用了其它對象的方法,其它對象又調用了更多對象,最後形成了一個無比巨大的調用樹。

很多比較舊的描述單元測試的書裏寫了一些傳統的辦法,這類方法基本上是先對耦合的部分做模擬,再對結果部分做斷言。例如可以通過繼承來自己做一個假的stub對象,最終用assert的方式驗證正確性。但是這相當於對於每種假設都要做一個假的對象,而且對結果進行驗證也比較複雜:比如我要驗證“更新”操作是否真的調用了dao層,那麼要自己在stub對象裏對調用進行計數,驗證時再對計數進行斷言,非常繁瑣。

後來出現了一些mock框架,比如java的JMockit、EasyMock,或者Mockito。利用這類框架可以相對比較輕鬆的通過mock方式去做假設和驗證,相對於之前的方式有了質的飛躍,但是即使用上這類框架,遇到複雜的業務代碼往往也無能爲力。

而往往新人的代碼質量往往不高,尤其是對代碼的拆分和邏輯的抽象還處於懵懂階段。要對這類代碼寫單測,即使是工作了3,4年的高級碼農也是一個挑戰,對新人來說幾乎是不可能完成的任務。這也讓很多新人有了“寫單測很難”的感覺。

所以在這裏需要強調一個觀點,寫單元測試的難易程度跟代碼的質量關係最大,並且是決定性的。項目裏無論用了哪個測試框架都不能解決代碼本身難以測試的問題,所以如果你遇到的是“我的代碼裏依賴的東西太多了所以寫不出來單測”這樣的問題的話,需要去看的是如何設計和重構代碼,而不是這篇文章。

2.3. 推薦閱讀

  • 重構-改善既有代碼的設計
  • 修改代碼的藝術
  • 敏捷軟件開發:原則、模式與實踐

3. Spock是什麼

3.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的幾個特性,還要理解幾個關鍵名詞:

3.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的方便功能。

3.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其中一部分思想的實現手段:通過某種規範說明語言去描述程序“應該”做什麼,再通過一個測試框架讀取這些描述、並驗證應用程序是否符合預期。

3.1.3. 單元測試的運行場景

測試只有被執行之後纔會有價值,這裏就涉及一個“什麼時候執行單元測試”的問題。

  1. 被接觸最多的就是在IDE中執行單元測試,java程序員比較幸運,主流的java IDE都可以很好的集成了單元測試功能,單元測試代碼自動生成、測試覆蓋率檢查等功能也都成了IDE的標配。這些功能都能讓程序員在編寫代碼的時候直接可以運行單元測試得到反饋。

  2. 其次,主流的構建工具(如maven、gradle)中也都實現了運行單元測試的功能,在生成二進制包之前可以對代碼進行迴歸測試,這些構建工具都可以通過命令行調用,這是自動化構建的前提。

  3. 在此之上,依託於構建工具提供的自動化特性,在持續集成、持續部署的過程中可以執行自動化構建,在自動化構建的過程中通過構建工具執行單元測試,這是持續集成的流程中的重要步驟。

3.2. Spock與現有框架的對比

3.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函數寫的

最終,團隊要接受“雖然確實寫了單測,然而這並沒有什麼卵用”的結果。

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

3.2.3. 爲什麼不用spock

用了一段時間的spock後,我也總結了幾個不用spock的理由:

  • 框架相對比較新,IDE的支持(尤其是eclipse)不如其它成熟的框架
  • groovy語言本身的compiler更新比較快,偶爾有坑(版本不兼容等)
  • 需要了解groovy語言
  • 與其它java的測試框架風格相差比較大,需要適應

當然,這些理由比起spock提供的易於開發和維護的單元測試代碼來說,都是可以忽略的。

4. 使用Spock

寫到這裏,還是要聚焦一下這篇文章要討論的問題:如何用spock框架編寫單元測試,在此之前再強調一下:

  • 單元測試不一定非要使用spock,但是其它框架寫出的單元測試代碼遠沒有用spock框架優雅。
  • spock框架並不只能寫單元測試,它也可以寫集成測試,甚至性能測試,但是後兩者spock相對於其它框架來說沒有什麼優勢。

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

4.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的目錄,之後將它設爲測試源碼目錄。

  4. 創建一個簡單的類:

    public class Sum {
        public int sum(int first, int second) {
            return first + second;
        }
    }
  5. 創建測試類,可以手工創建,也可以使用IDEA的輔助創建:

  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. 運行一下測試:

至此,一個最簡單的spock測試就寫完了。

4.3. Spock中的概念

4.3.1. Specification

在Spock中,待測系統(system under test; SUT) 的行爲是由規格(specification) 所定義的。在使用Spock框架編寫測試時,測試類需要繼承自Specification類。

4.3.2. Fields

Specification類中可以定義字段,這些字段在運行每個測試方法前會被重新初始化,跟放在setup()裏是一個效果。

def obj = new ClassUnderSpecification()
def coll = new Collaborator()

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

4.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
}

4.3.5. blocks

每個feature method又被劃分爲不同的block,不同的block處於測試執行的不同階段,在測試運行時,各個block按照不同的順序和規則被執行,如下圖:

下面分別解釋一下各個block的用途。

4.3.6. Setup Blocks

setup也可以寫成given,在這個block中會放置與這個測試函數相關的初始化程序,如:

setup:
def stack = new Stack()
def elem = "push me"

一般會在這個block中定義局部變量,定義mock函數等。

4.3.7. When and Then Blocks

when與then需要搭配使用,在when中執行待測試的函數,在then中判斷是否符合預期,如:

when:
stack.push(elem)  

then:
!stack.empty
stack.size() == 1
stack.peek() == elem

4.3.7.1. 斷言

條件類似junit中的assert,就像上面的例子,在then或expect中會默認assert所有返回值是boolean型的頂級語句。如果要在其它地方增加斷言,需要顯式增加assert關鍵字,如:

def setup() {
  stack = new Stack()
  assert stack.empty
}

4.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)
}

4.3.8. Expect Blocks

expect可以看做精簡版的when+then,如:

when:
def x = Math.max(1, 2)  

then:
x == 2

可以簡化爲:

expect:
Math.max(1, 2) == 2

4.3.9. Cleanup Blocks

函數退出前做一些清理工作,如關閉資源等。

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

4.4. Interaction Based Testing

對於測試來說,除了能夠對輸入-輸出進行驗證之外,還希望能驗證模塊與其他模塊之間的交互是否正確,比如“是否正確調用了某個某個對象中的函數”;或者期望被調用的模塊有某個返回值,等等。

各類mock框架讓這類驗證變得可行,而spock除了支持這類驗證,並且做的更加優雅。如果你還不清楚mock是什麼,最好先去簡單瞭解一下,網上的資料非常多,這裏就不展開了。

4.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框架應該會被這個特性深深的吸引住。

4.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"

4.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,不限制次數。

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

具體的使用方法如果有興趣可以參考官方文檔。

4.7. 更多

至此,讀者應該對Spock的主要功能和使用方法應該有個粗略的認識。如果希望實際使用spock,推薦讀一下官方的文檔,寫的比較清晰,並且其中引用的一些文檔也都值得一讀:

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

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

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

需要再強調一下:現實中的場景絕對會比這些例子複雜(比如要mock一個private函數,或者全局變量,或者靜態函數,等等),但是此時更好的思路並不是壓榨框架的功能,而應該是去思考代碼的設計是否出了問題。

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