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

来源:https://www.cnblogs.com/kiwifly/archive/2019/11/03/11789468.html
-Advertisement-
Play Games

0. 為什麼人人都討厭寫單測 在之前的關於 "swagger" 文章里提到過, 程式員最討厭的兩件事,一件是別人不寫文檔,另一件就是自己寫文檔。這裡如果把文檔換成單元測試也同樣成立。 每個開發人員都明白單元測試的作用,也都知道代碼覆蓋率越高越好。高覆蓋率的代碼,相對來說出現 BUG 的概率就越低,在 ...


image.png

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** 就可以了。
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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在比較絢麗多彩的網站或者業務邏輯比較豐富的程式設計過程中,圖片的相關操作時必不少的,尤其時圖片的上傳。還沒有徹底擺脫紙質辦公可能需要將紙質的文件備份上傳,網站的建設可能需要上傳用戶頭像、圖片描述等等,這些都需要將圖片從本地上傳到網上(伺服器)。下麵將介紹筆者今天在做圖片上傳過程中所遇到的坑~ 一、業 ...
  • Lambda(二)lambda表達式使用 Lambda 表達式組成: Lambda表達式需要有與之相匹配的預定義函數式介面: 簡單使用案例,source code 如下 假如,現在要對Apple的list進行排序(常規vsLambda): 自定義使用,source code如下 ...
  • 新聞 "Elmish.WPF教程" "介紹Orleans 3.0" "GC配置歷史" "介紹ONNX運行時1.0" "介紹微軟Q&A(預覽)" "使用App中心持續佈署與監控你的UWP,WPF與Windows Forms應用" 視頻及幻燈片 "介紹F " ".NET設計審查:ARM Intrinsi ...
  • [TOC] 簡介 SQLAlchemy是一個基於Python實現的ORM框架。該框架建立在 DB API之上,使用關係對象映射進行資料庫操作,簡言之便是:將類和對象轉換成SQL,然後使用數據API執行SQL並獲取執行結果。 安裝 組成部分 Engine:框架的引擎 Connection Poolin ...
  • 一、線程安全 線程安全的概念:當多個線程訪問某一個類(對象或方法)時。這個類始終都能表現出正確的行為那麼這個類(對象或方法)就是線程安全的。 synchronized:可以在任何對象及方法上加鎖,而加鎖的這段代碼稱為“互斥區”或“臨界區” 示例:【com.study.base.thread.a_sy ...
  • 二叉查找樹是將一組無序的數據構建成一顆有序數據的樹,其設計思想與二分法類似。很好的提高了海量數據查找效率,使得由從頭遍歷到尾的方式轉為二分查找的方式,時間複雜度從O(n)降低為O(log(n))。 ...
  • 搭建 vue cli 腳手架 1. 安裝 git 2. 安裝 node 並配置環境變數,使用 zip 版本 3. 使用淘寶鏡像 4. 安裝 node 模塊 cnpm 5. 全局安裝 vue cli 6. 使用 webpack 骨架初始化應用 ,就像 maven 骨架一樣 7. 運行項目 引入 Ele ...
  • 描述:Spring框架中,@Resource註解報錯,在書寫時沒有自動提示 解決方法:因為maven配置文件的pom.xml文件中缺少javax.annotation的依賴,在pom.項目路中加入依賴即可 <!-- Javax Annotation --> <dependency> <groupId ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...