1. 程式人生 > >【spock】單測竟然可以如此絲滑

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

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

在之前的關於swagger文章裡提到過,程式設計師最討厭的兩件事,一件是別人不寫文件,另一件就是自己寫文件。這裡如果把文件換成單元測試也同樣成立。
每個開發人員都明白單元測試的作用,也都知道程式碼覆蓋率越高越好。高覆蓋率的程式碼,相對來說出現 BUG 的概率就越低,在線上執行就越穩定,接的鍋也就越少,就也不會害怕測試同事突然的關心。
既然這麼多好處,為什麼還會討厭他呢?至少在我看來,單測有如下幾點讓我喜歡不起來的理由。
第一,要額外寫很多很多的程式碼,一個高覆蓋率的單測程式碼,往往比你要測試的,真正開發的業務程式碼要多,甚至是業務程式碼的好幾倍。這讓人覺得難以接受,你想想開發 5 分鐘,單測 2 小時是什麼樣的心情。而且並不是單測寫完就沒事了,後面業務要是變更了,你所寫的單測程式碼也要同步維護。

第二,即使你有那個耐心去寫單測,但是在當前這個拼速度擠時間的大環境下,會給你那麼多寫單測的時間嗎?寫一個單測的時間可以實現一個需求,你會如何去選?
第三,寫單測通常是一件很無趣的事,因為他比較死,主要目的就是為了驗證,相比之下他更像是個體力活,沒有真正寫業務程式碼那種創造的成就感。寫出來,驗證不出bug很失落,白寫了,驗證出bug又感到自己是在打自己臉。

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

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

最後得出了一個無可奈何的結論,單測是個讓人又愛又恨的東西,是不想做但又不得不做的事情。雖然我們沒辦法改變要寫單測這件事,但是我們可以改變怎麼去寫單元測試這件事。

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

當然,本文不是教你用旁門左道的方法提高程式碼覆蓋率。而是通過一個神奇的框架 spock 去提高你編寫單元測試的效率。spock 這名稱來源,個人猜測是因為《星際迷航》的同名人物(封面圖)。那麼spock 是如何提高編寫單測的效率呢?我覺得有以下幾點:
第一,他可以用更少的程式碼去實現單元測試,讓你可以更加專注於去驗證結果而不是寫單測程式碼的過程。那麼他又是如何做到少寫程式碼這件事呢?原來他使用一種叫做 groovy 的魔法。

groovy 其實是一門基於 jvm 的動態語言。可以簡單的理解成跑在 jvm 上的 python 或 js。說到這裡,可能沒有接觸過動態語言的同學,對它們都會有一個比較刻板的印象,太過於靈活,很容易出現問題,且可維護性差,所以有了那一句『動態一時爽,全家 xxx』的梗。首先,這些的確是他的問題,嚴格的說是使用不當時才帶來的問題。所以主要還是看使用的人。比如安卓領域的官方依賴管理工具 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** 就可以了。

另外就是 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 跑完之後會如下所示:

那麼現在我們看看如果有用例不通過會怎麼樣,把上面程式碼的最後一個 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 會出現如下顯示

左邊標註出來的是用例執行結果,可以看出來雖然有 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
    }

執行結果如下: 
通過新增** @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 或者一些模板引擎中使用的方法。# 號拼接宣告的變數就可以了,執行後結果如下。

這下更清晰了。
另外一點,就是 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