Spock測試套件入門

Spock測試套件

Spock套件基於一個單元測試框架,它有比junit更爲簡潔高效的測試語法。html

核心概念

總體認識

Spock中一個單元測試類名叫Specification。全部的單元測試類,都須要繼承Specificationjava

class MyFirstSpecification extends Specification {
  // fields
  // fixture methods
  // feature methods
  // helper methods
}

對於spock來講,Specification表明了一個軟件、應用、類的使用規範,其中的全部單元測試方法,被稱爲feature,即功能。sql

一個feature method的執行邏輯大概是以下幾步:數據庫

  1. setup 設置該功能的前置配置
  2. stimulus 提供一個輸入,觸發該功能
  3. response 描述你指望該功能的返回值
  4. cleanup 清理功能的前置配置

因此,對spock來講,一個單元測試,實際上是這個軟件應用提供的功能使用規範,這個規範中提供了每一個功能的使用說明書,輸入什麼,會獲得什麼,大致是按這個見解,去寫單元測試的。express

前置、後置

就像junit同樣,咱們能夠對整個單元測試類作一些前置,並清理。也能夠對每一個單元測試的方法作一些前置後清理。框架

其跟Junit的類比關係爲dom

setupSpec 對應 @BeforeClass
setup 對應 @Before
cleanup 對應 @After
cleanupSpec 對應 @AfterClass

同時因爲Spock的單元測試自己是會集成Specification 父類的,因此父類中的前置、後置方法也會被調用,不過不用顯示調用,會自動調用。性能

一個測試功能方法執行時,其總體的執行順序爲:單元測試

super.setupSpec

sub.setupSpec

super.setup

sub.setup

**feature method

sub.cleanup

super.cleanup

sub.cleanupSpec

super.cleanupSpec

同junit的類比

Feature 方法

blocks

feature的具體寫法有不少的block組成,這些block對應的feature方法自己的四個階段(setup, stimulus, reponse, cleanup) 。每一個block對應階段示意圖測試

典型的用法

def '測試++'(){
        given:
            def x = 5
        when: def result = calculateService.plusPlus(x)
        then: result == 6
    }
  • given也能夠寫成setup,feature方法裏的given其實跟外面的setup方法功能同樣,都是作測試功能的前置設置。只是單獨的setup方法,是用來寫對每一個測試feature都有用的測試。只跟當前feature相關的設置,請放在feature方法內的given標籤
  • when 標籤用來實際調用想要測試的feature
  • then 中對when的調用返回進行結果驗證,這裏不須要寫斷言,直接寫表達式就是斷言

異常condition

then中的斷言在spock中叫condition。好比Java中的Stack在沒有元素時,進行Popup,則會EmptyStackException異常。咱們指望它確實會拋出這個異常,那麼寫法以下

def '異常2'() {
        given:
        def stack = new Stack()
        when:
        def result = stack.pop()
        then:
        EmptyStackException e = thrown()
    }

它並不會拋出EmptyStackException,咱們要測試這個預期的話,代碼以下:

def '異常2'() {
        given:
        def stack = new Stack()
        stack.push("hello world")
        when:
        stack.pop()
        then:
        EmptyStackException e = notThrown()
    }

then和expect的區別

前面說了when block用來調用,then用來判斷預期結果。但有的時候,咱們的調用和預期判斷並不複雜,那麼能夠用expect將二者合在一塊兒,好比如下兩段代碼等價

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

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

cleanup block的用法

def 'cleanup'() {
        given:
        def file = new File("/some/path")
        file.createNewFile()

        // ...

        cleanup:
        file.delete()
    }

用於清理feature測試執行後的一些設置,好比打開的文件連接。該操做即使測試的feature出異常,依然會被調用

一樣,若是多個測試feature都須要這個cleanup.那麼建議將cleanup的資源提到setup方法中,並在cleanup方法中去清理

測試用例中的文本描述

爲了讓單元測試可讀性更高,能夠將測試方法中每一部分用文本進行描述,多個描述能夠用and來串聯

def '異常2'() {
        given:'設置stack對象'
        def stack = new Stack()

        and:'其它變量設施'
        stack.push('hello world')


        when:'從stack中彈出元素'
        def result = stack.pop()

        then:'預期會出現的異常'
        EmptyStackException e = thrown()
    }

Extension

spock經過標註來擴充單元測試的功能

@Timeout指定一個測試方法,或一個設置方法最長能夠執行的時間,用於對性能有要求的測試
@Ignore用於忽略當前的測試方法
@IgnoreRest忽略除當前方法外的全部方法,用於想快速的測一個方法
@FailsWith 跟exception condition相似

數據驅動測試

數據表

對於有些功能邏輯,其代碼是同樣的,只是須要測試不一樣輸入值。按照先前的介紹,最簡潔的寫法爲:

def "maximum of two numbers1"() {
        expect:
        // exercise math method for a few different inputs
        Math.max(1, 3) == 3
        Math.max(7, 4) == 4
        Math.max(0, 0) == 1
    }

缺點:

  1. Math.max代碼須要手動調用三次
  2. 第二行出錯後,第三行不會被執行
  3. 數據和代碼耦合在一塊兒,不方便數據從其它地方獨立準備

因此spock引入了數據表的概念,將測試數據和代碼分開。典型實例以下:

class MathSpec extends Specification {
  def "maximum of two numbers"() {
    expect:
    Math.max(a, b) == c

    where:
    a | b || c
    1 | 3 || 3
    7 | 4 || 7
    0 | 0 || 0
  }
}
  • where語句中,定義數據表。第一行是表頭,定義這一列所屬的變量。
  • 實際代碼調用,只須要調用一次。代碼中的變量跟數據表中的變量必須一一對應
  • 看似一個方法,實際上執行時,spock會根據數據表中的行數,循環迭代執行代碼。每一行都是獨立於其他行執行,因此有setup和cleanup塊,對每個行的都會重複執行一次
  • 而且某一行的數據出錯,並不影響其他行的執行

另外的寫法

def "maximum of two numbers"(int a, int b ,int c) {
        expect:
        Math.max(a, b) == c

        where:
        a | b | c
        1 | 3 | 3
        7 | 4 | 4
        0 | 0 | 1
    }
  • 變量能夠在方法參數中聲明,但不必
  • 數據表能夠所有用一個豎線來分割,但沒法像兩個豎線同樣清晰的分割輸入和輸出

更清晰的測試結果展現

class MathSpec extends Specification {
  def "maximum of two numbers"() {
    expect:
    Math.max(a, b) == c

    where:
    a | b || c
    1 | 3 || 3
    7 | 4 || 4
    0 | 0 || 1
  }
}

以上測試代碼,數據表中的後兩行會執行失敗。但從測試結果面板中,不能很好的看到詳細結果

使用@Unroll能夠將每一個迭代的執行結果輸出

能夠看到面板中實際輸出的文本爲測試方法的名稱。若是像在輸出中加上輸入輸出的變量,來詳細展現每一個迭代,能夠在方法名中使用佔位符#variable來引用變量的值。舉例以下:

@Unroll
    def "maximum of #a and #b is #c"() {
        expect:
        Math.max(a, b) == c

        where:
        a | b || c
        1 | 3 || 3
        7 | 4 || 4
        0 | 0 || 1
    }

更豐富的數據準備方式

前面的數據表顯示的將數據以表格的形式寫出來。實際上,數據在where block中的準備還有其它多種方式。

where:
a << [1, 7, 0]
b << [3, 4, 0]
c << [3, 7, 0]

從數據庫中查詢

@Shared sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")

def "maximum of two numbers"() {
  expect:
  Math.max(a, b) == c

  where:
  [a, b, c] << sql.rows("select a, b, c from maxdata")
}

使用groovy代碼賦值

where:
a = 3
b = Math.random() * 100
c = a > b ? a : b

以上幾種方式能夠混搭。

其中方法名也能夠以豐富的表達式引用where block中的變量

def "person is #person.age years old"() { 
  ...
  where:
  person << [new Person(age: 14, name: 'Phil Cole')]
  lastName = person.name.split(' ')[1]
}

基於交互的測試(Interaction Based Testing)

有的時候,咱們測試的功能,須要依賴另外的collaborators來測試。這種涉及到多個執行單元之間的交互,叫作交互測試

好比:

class Publisher {
  List<Subscriber> subscribers = []
  int messageCount = 0
  void send(String message){
    subscribers*.receive(message)
    messageCount++
  }
}

interface Subscriber {
  void receive(String message)
}

咱們想測Publisher,但Publisher有個功能是是發消息給全部的Subscriber。要想測試Publisher的發送功能確實ok,那麼須要測試Subscriber的確能收到消息。

使用一個實際的Subscriber實現當然能實現這個測試。但對具體的Subscriber實現形成了依賴,這裏須要Mock。使用spock的測試用例以下:

class PublisherTest extends Specification{
    Publisher publisher = new Publisher()
    Subscriber subscriber = Mock()
    Subscriber subscriber2 = Mock() //建立依賴的Subscriber Mock

    def setup() {
        publisher.subscribers << subscriber // << is a Groovy shorthand for List.add()
        publisher.subscribers << subscriber2
    }


    def "should send messages to all subscribers"() {
        when:
        publisher.send("hello") //調用publisher的方法
        then:
        1*subscriber.receive("hello") //指望subscriber的receive方法能被調用一次
        1*subscriber2.receive("hello")//指望subscriber1的receive方法能被調用一次
    }
}

以上代碼的目的是經過mock來測試當Publisher的send的方法被執行時,且執行參數是'hello'時,subscriber的receive方法必定能被調用,且入參也爲‘hello’

對依賴Mock的調用指望,其結構以下

1 * subscriber.receive("hello")
|   |          |       |
|   |          |       argument constraint
|   |          method constraint
|   target constraint
cardinality

cardinality
定義右邊指望方法執行的次數,這裏是指望執行一次,可能的寫法有以下:

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

target constraint
定義被依賴的對象。可能的寫法以下

1 * subscriber.receive("hello") // a call to 'subscriber'
1 * _.receive("hello")          // a call to any mock object

Method Constraint
定義在上述對象上指望被調用的方法,可能的寫法以下:

1 * subscriber.receive("hello") // a method named 'receive'
1 * subscriber./r.*e/("hello")  // a method whose name matches the given regular expression
                                // (here: method name starts with 'r' and ends in 'e')

Argument Constraints
對被調用方法,指望的入參進行定義。可能寫法以下:

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(endsWith("lo")) // any non-null argument that is-a String
1 * subscriber.receive({ it.size() > 3 && it.contains('a') })
// an argument that satisfies the given predicate, meaning that
// code argument constraints need to return true of false
// depending on whether they match or not
// (here: message length is greater than 3 and contains the character a)

一些通配符

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

嚴格模式(Strict Mocking)

when:
publisher.publish("hello")

then:
1 * subscriber.receive("hello") // demand one 'receive' call on 'subscriber'
_ * auditing._                  // allow any interaction with 'auditing'
0 * _                           // don't allow any other interaction

默認狀況下,你對Mock實例的方法的調用,會返回該方法返回值的默認值,好比該方法返回的是布爾型,那麼你你調用mock實例中的該方法時,將返回布爾型的默認值false.

若是咱們但願嚴格的限定Mock實例的各方法行爲,能夠經過上述代碼,對須要測試的方法顯示定義指望調用行爲,對其它方法設置指望一次都不調用。以上then block中的0 * _ 便是定義這種指望。當除subscriber中的receive和auditing中的全部方法被調用時,該單元測試會失敗,由於這不符合咱們對其它方法調用0次的指望

調用順序

then:
2 * subscriber.receive("hello")
1 * subscriber.receive("goodbye")

以上兩個指望被調用的順序是隨機的。若是要保證調用順序,使用兩個then

then:
2 * subscriber.receive("hello")

then:
1 * subscriber.receive("goodbye")

Stubbing 定義方法返回

前面的interaction mock是用來測試被mock的對象,指望方法的調用行爲。好比入參,調用次數。

而stubbing則用來定義被mock的實例,在調用時返回的行爲

總結,前者定義調用行爲指望,後者定義返回行爲指望。且Interaction test 測試的是執行指望或斷言。的stubbing則是用來定義mock的模擬的行爲。

因此stubbing 對mock方法返回值的定義應該放在given block. 而對mock方法自己的調用Interaction test 應該放在then block中。因此stubbing對返回值的定義至關於在定義測試的測試數據。

Stubbing的使用場景也很明確。假設Publisher須要依賴Subscriber方法的返回值,再作下一步操做。那咱們就須要對Subscriber的返回值進行mock,來測試不一樣返回值對目標測試代碼(feature)的行爲。

咱們將上述Subscriber接口對應的方法添加一個返回值

class Publisher {
        Subscriber subscriber
        int messageCount = 0

        int send(String message){
            if(subscriber.receive(message) == 'ok') {
                this.messageCount++
            }
            return messageCount
        }
    }

interface Subscriber {
    String receive(String message)
}

測試代碼舉例

Publisher publisher = new Publisher()
Subscriber subscriber = Mock()

def setup() {
    publisher.subscriber = subscriber
}

def "should send msg to subscriber"() {
     given:
     subscriber.receive("message1") >> "ok"

     when:
     def result = publisher.send("message1")

     then:
     result == 1
 }

以上代碼表示,模擬subscriber.receive被調用時,且調用參數爲message1,方法返回ok. 而此時指望(斷言)Publisher的send方法,返回的是1

stubbing 返回值結構

subscriber.receive(_) >> "ok"
|          |       |     |
|          |       |     response generator
|          |       argument constraint
|          method constraint
target constraint

注意這裏多了response generator,而且沒有interaction test中的Cardinality

各類返回值定義

返回固定值

subscriber.receive("message1") >> "ok"
subscriber.receive("message2") >> "fail"

順序調用返回不一樣的值

subscriber.receive(_) >>> ["ok", "error", "error", "ok"]

第一次調用返回ok,第二次、三次調用返回error。剩下的調用返回ok

根據入參計算返回值

subscriber.receive(_) >> { args -> args[0].size() > 3 ? "ok" : "fail" }
subscriber.receive(_) >> { String message -> message.size() > 3 ? "ok" : "fail" }

上述二者效果都同樣,都是對第一個入參的長度進行判斷,而後肯定返回值

返回異常

subscriber.receive(_) >> { throw new InternalError("ouch") }

鏈式返回值設定

subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"

前三次調用依次返回ok,fail,ok。第四次調用返回異常,以後的調用返回ok

將Interaction Mock和stubbing組合

1 * subscriber.receive("message1") >> "ok"
1 * subscriber.receive("message2") >> "fail"

這裏即定義了被mock 的subscriber其方法返回值,也定義了該方法指望被調用多少次。舉例:

Publisher publisher = new Publisher()
Subscriber subscriber = Mock()

def setup() {
    publisher.subscriber = subscriber
}

def "should send msg to subscriber"() {
    given:
    1*subscriber.receive("message1") >> "ok"

    when:
    def result = publisher.send("message1")

    then:
    result == 1
}

以上寫法,即測試了subscriber.receive被調用了一次,也測試了publisher.send執行結果爲1.若是將Interaction Mock和stubbing組合拆開,像下面這種寫法是不行的:

Publisher publisher = new Publisher()
Subscriber subscriber = Mock()

def setup() {
    publisher.subscriber = subscriber
}

def "should send msg to subscriber"() {
        given:
        subscriber.receive("message1") >> "ok"

        when:
        def result = publisher.send("message1")

        then:
        result == 1
        1*subscriber.receive("message1")
    }

如何建立單元測試類

方式一

像Junit同樣,在須要測試的類上,使用Idea的幫助快捷鍵,而後彈出

選擇指定的測試框架spock和路徑便可

方式二

直接在指定的測試目錄下,新建對應的測試類,注意是新建groovy class
在Idea中,groovy class的圖標是方塊,java class是圓形,注意區分

有可能建完後,對應的圖標是

,說明Ide沒有識別到這是個groovy 類,通常是因爲其代碼有問題,能夠打開該文件,把具體的錯誤修復,好比把註釋去掉之類的

參考資料

http://spockframework.org/spock/docs/1.1/all_in_one.html#_introduction

相關文章
相關標籤/搜索