1. 程式人生 > >Spock測試套件入門

Spock測試套件入門

[TOC] ### Spock測試套件 Spock套件基於一個單元測試框架,它有比junit更為簡潔高效的測試語法。 ### 核心概念 #### 整體認識 Spock中一個單元測試類名叫Specification。所有的單元測試類,都需要繼承Specification ``` class MyFirstSpecification extends Specification { // fields // fixture methods // feature methods // helper methods } ``` 對於spock來說,Specification代表了一個軟體、應用、類的使用規範,其中的所有單元測試方法,被稱為feature,即功能。 一個feature method的執行邏輯大概是如下幾步: 1. setup 設定該功能的前置配置 2. stimulus 提供一個輸入,觸發該功能 3. response 描述你期望該功能的返回值 4. cleanup 清理功能的前置配置 所以,**對spock來說,一個單元測試,其實是這個軟體應用提供的功能使用規範,這個規範中提供了每個功能的使用說明書,輸入什麼,會得到什麼**,大體是按這個看法,去寫單元測試的。 #### 前置、後置 就像junit一樣,我們可以對整個單元測試類做一些前置,並清理。也可以對每個單元測試的方法做一些前置後清理。 其跟Junit的類比關係為 ``` 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的類比 ![](https://img2020.cnblogs.com/blog/2007268/202009/2007268-20200918213904973-1052154928.png) ### Feature 方法 #### blocks feature的具體寫法有很多的block組成,這些block對應的feature方法本身的四個階段(setup, stimulus, reponse, cleanup) 。每個block對應階段示意圖 ![](https://img2020.cnblogs.com/blog/2007268/202009/2007268-20200918213917716-908409410.png) #### 典型的用法 ``` 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 } } ``` 以上測試程式碼,資料表中的後兩行會執行失敗。但從測試結果面板中,不能很好的看到詳細結果 ![](https://img2020.cnblogs.com/blog/2007268/202009/2007268-20200918213939884-1522419768.png) 使用`@Unroll`可以將每個迭代的執行結果輸出 ![](https://img2020.cnblogs.com/blog/2007268/202009/2007268-20200918213952263-1846954594.png) 可以看到面板中實際輸出的文字為測試方法的名稱。如果像在輸出中加上輸入輸出的變數,來詳細展示每個迭代,可以在方法名中使用佔位符`#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 } ``` ![](https://img2020.cnblogs.com/blog/2007268/202009/2007268-20200918214006929-325281759.png) #### 更豐富的資料準備方式 前面的資料表顯示的將資料以表格的形式寫出來。實際上,資料在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 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的幫助快捷鍵,然後彈出 ![](https://img2020.cnblogs.com/blog/2007268/202009/2007268-20200918214056491-1845880103.png) 選擇指定的測試框架spock和路徑即可 #### 方式二 直接在指定的測試目錄下,新建對應的測試類,注意是新建groovy class 在Idea中,groovy class的圖示是方塊,java class是圓形,注意區分 ![](https://img2020.cnblogs.com/blog/2007268/202009/2007268-20200918214104963-487643204.png) 有可能建完後,對應的圖示是 ![](https://img2020.cnblogs.com/blog/2007268/202009/2007268-20200918214115233-904295424.png) ,說明Ide沒有識別到這是個groovy 類,一般是由於其程式碼有問題,可以開啟該檔案,把具體的錯誤修復,比如把註釋去掉之類的 ### 參考資料 http://spockframework.org/spock/docs/1.1/all_in_one.html#_intro