【spock】單測竟然可以如此絲滑

  • 2019 年 11 月 3 日
  • 筆記

image.png

0. 為什麼人人都討厭寫單測

在之前的關於swagger文章里提到過,程式設計師最討厭的兩件事,一件是別人不寫文檔,另一件就是自己寫文檔。這裡如果把文檔換成單元測試也同樣成立。
每個開發人員都明白單元測試的作用,也都知道程式碼覆蓋率越高越好。高覆蓋率的程式碼,相對來說出現 BUG 的概率就越低,在線上運行就越穩定,接的鍋也就越少,就也不會害怕測試同事突然的關心。
既然這麼多好處,為什麼還會討厭他呢?至少在我看來,單測有如下幾點讓我喜歡不起來的理由。
第一,要額外寫很多很多的程式碼,一個高覆蓋率的單測程式碼,往往比你要測試的,真正開發的業務程式碼要多,甚至是業務程式碼的好幾倍。這讓人覺得難以接受,你想想開發 5 分鐘,單測 2 小時是什麼樣的心情。而且並不是單測寫完就沒事了,後面業務要是變更了,你所寫的單測程式碼也要同步維護。
第二,即使你有那個耐心去寫單測,但是在當前這個拼速度擠時間的大環境下,會給你那麼多寫單測的時間嗎?寫一個單測的時間可以實現一個需求,你會如何去選?
第三,寫單測通常是一件很無趣的事,因為他比較死,主要目的就是為了驗證,相比之下他更像是個體力活,沒有真正寫業務程式碼那種創造的成就感。寫出來,驗證不出bug很失落,白寫了,驗證出bug又感到自己是在打自己臉。

1. 為什麼人人又必須寫單測

所以得到的結論就是不寫單測?那麼問題又來了,出來混遲早是要還的,上線出了問題,最終責任人是誰?不是提需求的產品、不是沒發現問題的測試同學,他們頂多就是連帶責任。最該負責的肯定是寫這段程式碼的你。特別是對於那些從事金融、交易、電商等息息相關業務的開發人員,跟每行程式碼打交通的都是真金白銀。每次明星搞事,微博就掛,已經被傳為笑談,畢竟只是娛樂相關,如果掛的是支付寶、微信,那用戶就沒有那麼大的包容度了。這些業務如果出現嚴重問題,輕則掃地出門,然後整個職業生涯背負這個污點,重則直接從面向對象開發變成面向監獄開發。所以單元測試保護的不僅僅是程式,更保護的是寫程式的你
最後得出了一個無可奈何的結論,單測是個讓人又愛又恨的東西,是不想做但又不得不做的事情。雖然我們沒辦法改變要寫單測這件事,但是我們可以改變怎麼去寫單元測試這件事。

2. SPOCK 可以幫你改善單測體驗

當然,本文不是教你用旁門左道的方法提高程式碼覆蓋率。而是通過一個神奇的框架 spock 去提高你編寫單元測試的效率。spock 這名稱來源,個人猜測是因為《星際迷航》的同名人物(封面圖)。那麼spock 是如何提高編寫單測的效率呢?我覺得有以下幾點:
第一,他可以用更少的程式碼去實現單元測試,讓你可以更加專註於去驗證結果而不是寫單測程式碼的過程。那麼他又是如何做到少寫程式碼這件事呢?原來他使用一種叫做 groovy 的魔法。
groovy 其實是一門基於 jvm 的動態語言。可以簡單的理解成跑在 jvm 上的 python 或 js。說到這裡,可能沒有接觸過動態語言的同學,對它們都會有一個比較刻板的印象,太過於靈活,很容易出現問題,且可維護性差,所以有了那一句『動態一時爽,全家 xxx』的梗。首先,這些的確是他的問題,嚴格的說是使用不當時才帶來的問題。所以主要還是看使用的人。比如Android領域的官方依賴管理工具 gradle 就是基於 groovy 開發的。
另外不要誤以為我學這門框架,還要多學一門語言,成本太大。其實大可不必擔心,你如果會 groovy 當然更好,如果不會也沒有關係。因為 groovy 是基於 java 的,所以完全可以放心大膽的使用 java 的語法,某些要用到的 groovy 獨有的語法很少,而且後面都會告訴你。
第二,他有更好的語義化,讓你的單測程式碼可讀性更高。
語義化這個詞可能不太好理解。舉兩個例子來說吧,第一個是語義化比較好的語言 — HTML。他的語法特點就是標籤,不同的類型放在不同的標籤里。比如 head 就是頭部的資訊,body 是主體內容的資訊,table 就是表格的資訊,對於沒有編程經驗的人來說,也可以很容易理解。第二個是語義化比較差的語言 — 正則。他可以說基本上沒有語義這種東西,由此導致的直接問題就是,即使是你自己的寫的正則,幾天之後你都不知道當時寫的是什麼。比如下面這個正則,你能猜出他是什麼意思嗎?(可以留言回復)

((?:(?:25[0-5]|2[0-4]d|[01]?d?d).){3}(?:25[0-5]|2[0-4]d|[01]?d?d))

3. 領略 SPOCK 的魔法

3.1 引入依賴

        <!--如果沒有使得 spring boot,以下包可以省略-->          <dependency>              <groupId>org.springframework.boot</groupId>              <artifactId>spring-boot-starter-test</artifactId>              <scope>test</scope>          </dependency>          <!--引入spock 核心包-->          <dependency>              <groupId>org.spockframework</groupId>              <artifactId>spock-core</artifactId>              <version>1.3-groovy-2.5</version>              <scope>test</scope>          </dependency>          <!--引入spock 與 spring 集成包-->          <dependency>              <groupId>org.spockframework</groupId>              <artifactId>spock-spring</artifactId>              <version>1.3-groovy-2.5</version>              <scope>test</scope>          </dependency>          <!--引入 groovy 依賴-->          <dependency>              <groupId>org.codehaus.groovy</groupId>              <artifactId>groovy-all</artifactId>              <version>2.5.7</version>              <scope>test</scope>          </dependency>
說明

注釋已經標明,第一個包是 spring boot 項目需要使用的,如果你只是想使用 spock,只要最下面 3 個即可。其中第一個包 spock-core 提供了 spock 的核心功能,第二個包 spock-spring 提供了與 spring 的集成(不用 spring 的情況下也可以不引入)。 注意這兩個包的版本號 -> 1.3-groovy-2.5。第一個版本號 1.3 其實代表是 spock 的版本,第二個版本號代表的是 spock 所要依賴的 groovy 環境的版本。最後一個包就是我們要依賴的 groovy 。

3.2 準備基礎測試類

3.2.1 Calculator.java

/*   * *   *  * blog.coder4j.cn   *  * Copyright (C) 2016-2019 All Rights Reserved.   *   */  package cn.coder4j.study.example.spock;    /**   * @author buhao   * @version Calculator.java, v 0.1 2019-10-30 10:34 buhao   */  public class Calculator {        /**       * 加操作       *       * @param num1       * @param num2       * @return       */      public static int add(int num1, int num2) {          return num1 + num2;      }        /**       * 整型除操作       *       * @param num1       * @param num2       * @return       */      public static int divideInt(int num1, int num2) {          return num1 / num2;      }        /**       * 浮點型操作       * @param num1       * @param num2       * @return       */      public static double divideDouble(double num1,  double num2){          return num1 / num2;      }  }
說明

這是一個很簡單的計算器類。只寫了三個方法,一個是加法的操作、一個整型的除法操作、一個浮點類型的除法操作。

3.3 開始單測 Calculator.java

3.3.1 創建單測類 CalculatorTest.groovy

class CalculatorTest extends  Specification {    }
說明

這裡一定要注意,之前我們已經說了 spock 是基於 groovy 。所以單測類的後綴不是 .java 而** .groovy。千萬不要創建成普通 java 類了。否則創建沒有問題,但是寫一些 groovy 語法會報錯。如果你用的是 IDEA 可以通過如下方式創建,以前創建 Java 類我們都是選擇第一個選項,現在我們選擇第三個 Groovy Class** 就可以了。
image.png
另外就是 spock 的測試類需要繼承 spock.lang.Specification 類。

3.3.2 驗證加操作 – expect

    def "test add"(){          expect:          Calculator.add(1, 1) == 2      }
說明

def 是 groovy 的關鍵字,可以用來定義變數跟方法名。後面 "test add" 是你單元測試的名稱,也可以用中文。最後重點說明的是 expect 這個關鍵字。
expect 字面上的意思是期望,我們期望什麼樣的事情發生。在使用其它單測框架時,與之類似的是 assert 。比如 _Assert.assertEquals(_Calculator.add(_1 + 1), 2) _這樣,表示我們斷言加操作傳入1 與 1 相加結果為 2。如果結果是這樣則用例通過,如果不是則用例失敗。這與我們上面的程式碼功能上完成一致。
expect 的語法意義就是在 expect 的內,所有表達式成立則驗證通過,反之有任一個不成立則驗證失敗。這裡引入了一個的概念。怎麼理解 spock 的塊呢?我們上面說 spock 有良好的語義化及更好的閱讀性就是因為這個塊的作用。可以類比成 html 中的標籤。html 的標籤的範圍是兩個標籤之間,而 spock 更簡潔一點,從這個標籤開始到下一個標籤開始或程式碼結束的地方,就是他的範圍。我們只要看到 expect 這個標籤就明白,他的範圍內都是我們預期要得到的結果。

3.3.3 驗證加操作 – given – and

這裡程式碼比較簡單,參數我只使用了一次,所以直接寫死。如果想復用,我就得把這些參數抽成變數。這個時候可以使用 spock 的 given 塊。given 的語法意義相當於是一個初始化的程式碼塊。

    def "test add with given"(){          given:          def num1 = 1          def num2 = 1          def result = 2            expect:          Calculator.add(num1, num2) == result      }

當然你也可以像下面這樣寫,但是嚴重不推薦,因為雖然可以達到同樣的效果,但是不符合 spock 的語義。就像我們一般是在 head 裡面引入 js、css,但是你在 body 或者任何標籤里都可以引入,語法沒有問題但是破壞了語義,不便理解與維護。

    // 反倒      def "test add with given"(){          expect:          def num1 = 1          def num2 = 1          def result = 2          Calculator.add(num1, num2) == result      }

如果你還想讓語義更好一點,我們可以把參數與結果分開定義,這個時候可以使用 and 塊。它的語法功能可以理解成同他上面最近的一個標籤。

    def "test add with given and"(){          given:          def num1 = 1          def num2 = 1            and:          def result = 2            expect:          Calculator.add(num1, num2) == result      }

3.3.4 驗證加操作 – expect – where

看了上面例子,可能覺得 spock 只是語義比較好,但是沒有少寫幾行程式碼呀。別急,下面我們就來看 spock 的一大殺器 where

    def "test add with expect where"(){          expect:          Calculator.add(num1, num2) == result            where:          num1    |   num2    |   result          1       |   1       |   2          1       |   2       |   3          1       |   3       |   4      }

where 塊可以理解成準備測試數據的地方,他可以跟 expect 組合使用。上面程式碼里 expect 塊裡面定義了三個變數 num1、num2、result。這些數據我們可以在 where 塊里定義。where 塊使用了一種很像 markdown 中表格的定義方法。第一行或者說表頭,列出了我們要傳數據的變數名稱,這裡要與 expect 中對應,不能少但是可以多。其它行都是數據行,與表頭一樣都是通過 『 | 』 號分隔。通過這樣,spock 就會跑 3 次用例,分別是 1 + 2 = 2、1 + 2 = 3、1 + 3 = 4 這些用例。怎麼樣?是不是很方便,後面再擴充用例只要再加一行數據就可以了。 

3.3.5 驗證加操作 – expect – where – @Unroll

上面這些用例都是正常可以跑通的,如果是 IDEA 跑完之後會如下所示:
image.png
那麼現在我們看看如果有用例不通過會怎麼樣,把上面程式碼的最後一個 4 改成 5

    def "test add with expect where"(){          expect:          Calculator.add(num1, num2) == result            where:          num1    |   num2    |   result          1       |   1       |   2          1       |   2       |   3          1       |   3       |   5      }

再跑一次,IDEA 會出現如下顯示
image.png
左邊標註出來的是用例執行結果,可以看出來雖然有 3 條數據,其中 2 條數據是成功,但是只會顯示整體的成功與否,所以顯示未通過。但是 3 條數據,我怎麼知道哪條沒通過呢?
右邊標註出來的是 spock 列印的的錯誤日誌。可以很清楚的看到,在 num1 為 1,num2 為 3,result 為 5 並且 他們之間的判斷關係為 == 的結果是 false 才是正確的。 spock 的這個日誌列印的是相當歷害,如果是比較字元串,還會計算異常字元串與正確字元串之間的匹配度,有興趣的同學,可以自行測試。
嗯,雖然可以通過日誌知道哪個用例沒通過,但是還是覺得有點麻煩。spock 也知道這一點。所以他還同時提供了一個** @Unroll **註解。我們在上面的程式碼上再加上這個註解:

    @Unroll      def "test add with expect where unroll"(){          expect:          Calculator.add(num1, num2) == result            where:          num1    |   num2    |   result          1       |   1       |   2          1       |   2       |   3          1       |   3       |   5      }

運行結果如下: image.png
通過添加** @Unroll** 註解,spock 自動把上面的程式碼拆分成了 3 個獨立的單測測試,分別運行,運行結果更清晰了。
那麼還能更清晰嗎?當然可以,我們發現 spock 拆分後,每個用例的名稱其實都是你寫的單測方法的名稱,然後後面加一個數組下標,不是很直觀。我們可以通過 groovy 的字元串語法,把變數放入用例名稱中,程式碼如下:

    @Unroll      def "test add with expect where unroll by #num1 + #num2 = #result"(){          expect:          Calculator.add(num1, num2) == result            where:          num1    |   num2    |   result          1       |   1       |   2          1       |   2       |   3          1       |   3       |   5      }

如上,我們在方法名後加了一句 #num1 + #num2 = #result。這裡有點類似我們在 mybatis 或者一些模板引擎中使用的方法。# 號拼接聲明的變數就可以了,執行後結果如下。
image.png
這下更清晰了。
另外一點,就是 where 默認使用的是表格的這種形式:

        where:          num1    |   num2    |   result          1       |   1       |   2          1       |   2       |   3          1       |   3       |   5

很直觀,但是這種形式有一個弊端。上面 『 | 』 號對的這麼整齊。都是我一個空格一個 TAG 按出來的。雖然語法不要求對齊,但是逼死強迫症。不過,好在還可以有另一種形式:

    @Unroll      def "test add with expect where unroll arr by #num1 + #num2 = #result"(){          expect:          Calculator.add(num1, num2) == result            where:          num1 << [1, 1, 2]          num2 << [1, 2, 3]          result << [1, 3, 4]      }

可以通過 『<<』 符(注意方向),把一個數組賦給變數,等同於上面的數據表格,沒有表格直觀,但是比較簡潔也不用考慮對齊問題,這兩種形式看個人喜好了。

3.3.6 驗證整數除操作 – when – then

我們都知道一個整數除以0 會有拋出一個『/ by zero』異常,那麼如果斷言這個異常呢。用上面 expect 不太好操作,我們可以使用另一個類似的塊** when … then**。

    @Unroll      def "test int divide zero exception"(){          when:          Calculator.divideInt(1, 0)            then:          def ex = thrown(ArithmeticException)          ex.message == "/ by zero"      }

when … then 通常是成對出現的,它代表著當執行了 when 塊中的操作,會出現 then 塊中的期望。比如上面的程式碼說明了,當執行了 Calculator.divideInt(1, 0) 的操作,就一定會拋出 ArithmeticException 異常,並且異常資訊是 / by zero

3.4 準備Spring測試類

上面我們已經學會了 spock 的基礎用法,下面我們將學習與 spring 整合的知識,首先創建幾個用於測試的demo 類

3.4.1 User.java

/*   * *   *  * blog.coder4j.cn   *  * Copyright (C) 2016-2019 All Rights Reserved.   *   */  package cn.coder4j.study.example.spock.model;    import java.util.Objects;    /**   * @author buhao   * @version User.java, v 0.1 2019-10-30 16:23 buhao   */  public class User {      private String name;      private Integer age;      private String passwd;        public User(String name, Integer age, String passwd) {          this.name = name;          this.age = age;          this.passwd = passwd;      }        /**       * Getter method for property <tt>passwd</tt>.       *       * @return property value of passwd       */      public String getPasswd() {          return passwd;      }        /**       * Setter method for property <tt>passwd</tt>.       *       * @param passwd value to be assigned to property passwd       */      public void setPasswd(String passwd) {          this.passwd = passwd;      }        /**       * Getter method for property <tt>name</tt>.       *       * @return property value of name       */      public String getName() {          return name;      }        /**       * Setter method for property <tt>name</tt>.       *       * @param name value to be assigned to property name       */      public void setName(String name) {          this.name = name;      }        /**       * Getter method for property <tt>age</tt>.       *       * @return property value of age       */      public Integer getAge() {          return age;      }        /**       * Setter method for property <tt>age</tt>.       *       * @param age value to be assigned to property age       */      public void setAge(Integer age) {          this.age = age;      }        public User() {      }        @Override      public boolean equals(Object o) {          if (this == o) return true;          if (o == null || getClass() != o.getClass()) return false;          User user = (User) o;          return Objects.equals(name, user.name) &&                  Objects.equals(age, user.age) &&                  Objects.equals(passwd, user.passwd);      }        @Override      public int hashCode() {          return Objects.hash(name, age, passwd);      }  }

3.4.2 UserDao.java

/*   * *   *  * blog.coder4j.cn   *  * Copyright (C) 2016-2019 All Rights Reserved.   *   */  package cn.coder4j.study.example.spock.dao;    import cn.coder4j.study.example.spock.model.User;  import org.springframework.stereotype.Component;    import java.util.HashMap;  import java.util.Map;    /**   * @author buhao   * @version UserDao.java, v 0.1 2019-10-30 16:24 buhao   */  @Component  public class UserDao {        /**       * 模擬資料庫       */      private static Map<String, User> userMap = new HashMap<>();      static {          userMap.put("k",new User("k", 1, "123"));          userMap.put("i",new User("i", 2, "456"));          userMap.put("w",new User("w", 3, "789"));      }        /**       * 通過用戶名查詢用戶       * @param name       * @return       */      public User findByName(String name){          return userMap.get(name);      }  }

3.4.3 UserService.java

/*   * *   *  * blog.coder4j.cn   *  * Copyright (C) 2016-2019 All Rights Reserved.   *   */  package cn.coder4j.study.example.spock.service;    import cn.coder4j.study.example.spock.dao.UserDao;  import cn.coder4j.study.example.spock.model.User;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.stereotype.Service;    /**   * @author buhao   * @version UserService.java, v 0.1 2019-10-30 16:29 buhao   */  @Service  public class UserService {        @Autowired      private UserDao userDao;        public User findByName(String name){          return userDao.findByName(name);      }        public void loginAfter(){          System.out.println("登錄成功");      }        public void login(String name, String passwd){          User user = findByName(name);          if (user == null){              throw new RuntimeException(name + "不存在");          }          if (!user.getPasswd().equals(passwd)){              throw new RuntimeException(name + "密碼輸入錯誤");          }          loginAfter();      }  }

3.4.3 Application.java

/*   * *   *  * blog.coder4j.cn   *  * Copyright (C) 2016-2019 All Rights Reserved.   *   */    package cn.coder4j.study.example.spock;    import org.springframework.boot.SpringApplication;  import org.springframework.boot.autoconfigure.SpringBootApplication;    @SpringBootApplication  public class Application {        public static void main(String[] args) {          SpringApplication.run(Application.class, args);      }    }  

3.5 與 spring 集成測試

/*   * *   *  * blog.coder4j.cn   *  * Copyright (C) 2016-2019 All Rights Reserved.   *   */    package cn.coder4j.study.example.spock.service    import cn.coder4j.study.example.spock.model.User  import org.springframework.beans.factory.annotation.Autowired  import org.springframework.boot.test.context.SpringBootTest  import spock.lang.Specification  import spock.lang.Unroll    @SpringBootTest  class UserServiceFunctionTest extends Specification {        @Autowired      UserService userService        @Unroll      def "test findByName with input #name return #result"() {          expect:          userService.findByName(name) == result            where:          name << ["k", "i", "kk"]          result << [new User("k", 1, "123"), new User("i", 2, "456"), null]        }        @Unroll      def "test login with input #name and #passwd throw #errMsg"() {          when:          userService.login(name, passwd)            then:          def e = thrown(Exception)          e.message == errMsg            where:          name    |   passwd  |   errMsg          "kd"     |   "1"     |   "${name}不存在"          "k"     |   "1"     |   "${name}密碼輸入錯誤"        }  }  

spock 與 spring 集成特別的簡單,只要你加入了開頭所說的 spock-spring 和 spring-boot-starter-test。再於測試程式碼的類上加上 @SpringBootTest 註解就可以了。想用的類直接注入進來就可以了,但是要注意的是這裡只能算功能測試或集成測試,因為在跑用例時是會啟動 spring 容器的,外部依賴也必須有。很耗時,而且有時候外部依賴本地也跑不了,所以我們通常都是通過 mock 來完成單元測試。

3.6 與 spring mock 測試

/*   * *   *  * blog.coder4j.cn   *  * Copyright (C) 2016-2019 All Rights Reserved.   *   */    package cn.coder4j.study.example.spock.service    import cn.coder4j.study.example.spock.dao.UserDao  import cn.coder4j.study.example.spock.model.User  import spock.lang.Specification  import spock.lang.Unroll    class UserServiceUnitTest extends Specification  {        UserService userService = new UserService()      UserDao userDao = Mock(UserDao)        def setup(){          userService.userDao = userDao      }        def "test login with success"(){            when:          userService.login("k", "p")            then:          1 * userDao.findByName("k") >> new User("k", 12,"p")      }        def "test login with error"(){          given:          def name = "k"          def passwd = "p"            when:          userService.login(name, passwd)            then:          1 * userDao.findByName(name) >> null            then:          def e = thrown(RuntimeException)          e.message == "${name}不存在"        }        @Unroll      def "test login with "(){          when:          userService.login(name, passwd)            then:          userDao.findByName("k") >> null          userDao.findByName("k1") >> new User("k1", 12, "p")            then:          def e = thrown(RuntimeException)          e.message == errMsg            where:          name        |   passwd  |   errMsg          "k"         |   "k"     |   "${name}不存在"          "k1"        |   "p1"     |   "${name}密碼輸入錯誤"        }  }

spock 使用 mock 也很簡單,直接使用 Mock(類) 就可以了。如上程式碼 _UserDao userDao = Mock(UserDao) 。_上面寫的例子中有幾點要說明一下,以如下這個方法為例:

    def "test login with error"(){          given:          def name = "k"          def passwd = "p"            when:          userService.login(name, passwd)            then:          1 * userDao.findByName(name) >> null            then:          def e = thrown(RuntimeException)          e.message == "${name}不存在"        }

given、when、then 不用說了,大家已經很熟悉了,但是第一個 then 裡面的 1 * userDao.findByName(name) >> null 是什麼鬼?
首先,我們可以知道的是,一個用例中可以有多個 then 塊,對於多個期望可以分別放在多個 then 中。
第二, 1 * xx 表示 期望 xx 操作執行了 1 次。1 * userDao.findByName(name)** 就表現當執行 userService.login(name, passwd) 時我期望執行 1 次 userDao.findByName(name) 方法。如果期望不執行這個方法就是_0 * xx,這在條件程式碼的驗證中很有用,然後 >> null_ 又是什麼意思?他代表當執行了 userDao.findByName(name) 方法後,我讓他結果返回 null。因為 userDao 這個對象是我們 mock 出來的,他就是一個假對象,為了讓後續流程按我們的想法進行,我可以通過『 >>』 讓 spock 模擬返回指定數據。
第三,要注意第二個 then 程式碼塊使用
${name} 引用變數,跟標題的 #name** 是不同的。

3.7 其它內容

3.7.1 公共方法

方法名 作用
setup() 每個方法執行前調用
cleanup() 每個方法執行後調用
setupSpec() 每個方法類載入前調用一次
cleanupSpec() 每個方法類執行完調用一次

這些方法通常用於測試開始前的一些初始化操作,和測試完成後的清理操作,如下:

    def setup() {          println "方法開始前初始化"      }        def cleanup() {          println "方法執行完清理"      }        def setupSpec() {          println "類載入前開始前初始化"      }        def cleanupSpec() {          println "所以方法執行完清理"      }

3.7.2 @Timeout

對於某些方法,需要規定他的時間,如果運行時間超過了指定時間就算失敗,這時可以使用 timeout 註解

    @Timeout(value = 900, unit = TimeUnit.MILLISECONDS)      def "test timeout"(){          expect:          Thread.sleep(1000)          1 == 1      }

註解有兩個值,一個是 value 我們設置的數值,unit 是數值的單位。

3.7.3 with

    def "test findByName by verity"() {          given:          def userDao = Mock(UserDao)            when:          userDao.findByName("kk") >> new User("kk", 12, "33")            then:          def user = userDao.findByName("kk")          with(user) {              name == "kk"              age == 12              passwd == "33"          }        }

with 算是一個語法糖,沒有他之前我們要判斷對象的值只能,user.getXxx() == xx。如果屬性過多也是挺麻煩的,用 with 包裹之後,只要在花括弧內直接寫屬性名稱即可,如上程式碼所示。

4. 其它

4.1 完整程式碼

因為篇幅有限,無法貼完所有程式碼,完整程式碼已上傳 github

4.2 參考文檔

本文在瞻仰了如下部落客的精彩博文後,再加上自身的學習總結加工而來,如果本文在看的時候有不明白的地方可以看一下下方鏈接。

  1. Spock in Java 慢慢愛上寫單元測試
  2. 使用Groovy+Spock輕鬆寫出更簡潔的單測
  3. Spock 測試框架的介紹和使用詳解
  4. Spock 基於BDD測試
  5. Spock 官方文檔
  6. Spock測試框架
  7. spock-testing-exceptions-with-data-tables