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

image.png

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

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

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

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

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

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

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

3. 領(lǐng)略 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>
說明

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

3.2 準備基礎(chǔ)測試類

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 創(chuàng)建單測類 CalculatorTest.groovy

class CalculatorTest extends  Specification {
    
}
說明

這里一定要注意,之前我們已經(jīng)說了 spock 是基于 groovy 。所以單測類的后綴不是 .java 而** .groovy**。千萬不要創(chuàng)建成普通 java 類了。否則創(chuàng)建沒有問題,但是寫一些 groovy 語法會報錯。如果你用的是 IDEA 可以通過如下方式創(chuàng)建,以前創(chuàng)建 Java 類我們都是選擇第一個選項,現(xiàn)在我們選擇第三個 Groovy Class 就可以了。

image.png

另外就是 spock 的測試類需要繼承 **spock.lang.Specification **類。

3.3.2 驗證加操作 - expect

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

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

3.3.3 驗證加操作 - given - and

這里代碼比較簡單,參數(shù)我只使用了一次,所以直接寫死。如果想復(fù)用,我就得把這些參數(shù)抽成變量。這個時候可以使用 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
    }

如果你還想讓語義更好一點,我們可以把參數(shù)與結(jié)果分開定義,這個時候可以使用 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 塊可以理解成準備測試數(shù)據(jù)的地方,他可以跟 expect 組合使用。上面代碼里 expect 塊里面定義了三個變量 num1、num2、result。這些數(shù)據(jù)我們可以在 where 塊里定義。where 塊使用了一種很像 markdown 中表格的定義方法。第一行或者說表頭,列出了我們要傳數(shù)據(jù)的變量名稱,這里要與 expect 中對應(yīng),不能少但是可以多。其它行都是數(shù)據(jù)行,與表頭一樣都是通過 『 | 』 號分隔。通過這樣,spock 就會跑 3 次用例,分別是 1 + 2 = 2、1 + 2 = 3、1 + 3 = 4 這些用例。怎么樣?是不是很方便,后面再擴充用例只要再加一行數(shù)據(jù)就可以了。

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

上面這些用例都是正常可以跑通的,如果是 IDEA 跑完之后會如下所示:


image.png

那么現(xiàn)在我們看看如果有用例不通過會怎么樣,把上面代碼的最后一個 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 會出現(xiàn)如下顯示


image.png

左邊標注出來的是用例執(zhí)行結(jié)果,可以看出來雖然有 3 條數(shù)據(jù),其中 2 條數(shù)據(jù)是成功,但是只會顯示整體的成功與否,所以顯示未通過。但是 3 條數(shù)據(jù),我怎么知道哪條沒通過呢?
右邊標注出來的是 spock 打印的的錯誤日志??梢院芮宄目吹?,在 num1 為 1,num2 為 3,result 為 5 并且 他們之間的判斷關(guān)系為 == 的結(jié)果是 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
    }

運行結(jié)果如下:
image.png

通過添加** @Unroll** 注解,spock 自動把上面的代碼拆分成了 3 個獨立的單測測試,分別運行,運行結(jié)果更清晰了。
那么還能更清晰嗎?當然可以,我們發(fā)現(xiàn) spock 拆分后,每個用例的名稱其實都是你寫的單測方法的名稱,然后后面加一個數(shù)組下標,不是很直觀。我們可以通過 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 或者一些模板引擎中使用的方法。# 號拼接聲明的變量就可以了,執(zhí)行后結(jié)果如下。

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]
    }

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

3.3.6 驗證整數(shù)除操作 - when - then

我們都知道一個整數(shù)除以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 **通常是成對出現(xiàn)的,它代表著當執(zhí)行了 when 塊中的操作,會出現(xiàn) then 塊中的期望。比如上面的代碼說明了,當執(zhí)行了 _Calculator.divideInt(1, 0) _的操作,就一定會拋出 ArithmeticException 異常,并且異常信息是 / by zero。

3.4 準備Spring測試類

上面我們已經(jīng)學會了 spock 的基礎(chǔ)用法,下面我們將學習與 spring 整合的知識,首先創(chuàng)建幾個用于測試的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 {

    /**
     * 模擬數(shù)據(jù)庫
     */
    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-springspring-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 不用說了,大家已經(jīng)很熟悉了,但是第一個 then 里面的 **1 * userDao.findByName(name) >> null **是什么鬼?
首先,我們可以知道的是,一個用例中可以有多個 then 塊,對于多個期望可以分別放在多個 then 中。
第二, 1 * xx 表示 期望 xx 操作執(zhí)行了 1 次。1 * userDao.findByName(name) 就表現(xiàn)當執(zhí)行 _userService.login(name, passwd) 時我期望執(zhí)行 1 次 userDao.findByName(name) _方法。如果期望不執(zhí)行這個方法就是0 * xx,這在條件代碼的驗證中很有用,然后 >> null 又是什么意思?他代表當執(zhí)行了 userDao.findByName(name) 方法后,我讓他結(jié)果返回 null。因為 userDao 這個對象是我們 mock 出來的,他就是一個假對象,為了讓后續(xù)流程按我們的想法進行,我可以通過『 >>』 讓 spock 模擬返回指定數(shù)據(jù)。
第三,要注意第二個 then 代碼塊使用
${name} 引用變量,跟標題的 #name
是不同的。

3.7 其它內(nèi)容

3.7.1 公共方法

方法名 作用
setup() 每個方法執(zhí)行前調(diào)用
cleanup() 每個方法執(zhí)行后調(diào)用
setupSpec() 每個方法類加載前調(diào)用一次
cleanupSpec() 每個方法類執(zhí)行完調(diào)用一次

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

    def setup() {
        println "方法開始前初始化"
    }

    def cleanup() {
        println "方法執(zhí)行完清理"
    }

    def setupSpec() {
        println "類加載前開始前初始化"
    }

    def cleanupSpec() {
        println "所以方法執(zhí)行完清理"
    }

3.7.2 @Timeout

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

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

注解有兩個值,一個是 value 我們設(shè)置的數(shù)值,unit 是數(shù)值的單位。

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 包裹之后,只要在花括號內(nèi)直接寫屬性名稱即可,如上代碼所示。

4. 其它

4.1 完整代碼

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

4.2 參考文檔

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

  1. Spock in Java 慢慢愛上寫單元測試
  2. 使用Groovy+Spock輕松寫出更簡潔的單測
  3. Spock 測試框架的介紹和使用詳解
  4. Spock 基于BDD測試
  5. Spock 官方文檔
  6. Spock測試框架
  7. spock-testing-exceptions-with-data-tables
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容