GoConvey框架使用指南

序言

在軟件開發(fā)中,產品代碼的正確性通過測試代碼來保證,而測試代碼的正確性誰來保證?答案是毫無爭議的,肯定是程序員自己。這就要求測試代碼必須足夠簡單且表達力強,讓錯誤無處藏身。我們要有一個好鼻子,能夠嗅出測試的壞味道,及時的進行測試重構,從而讓測試代碼易于維護。筆者從大量的編碼實踐中感悟道:雖然能寫出好的產品代碼的程序員很牛,但能寫出好的測試代碼的程序員更牛,尤其對于TDD實踐。

要寫出好的測試代碼,必須精通相關的框架。對于Golang程序員來說,至少需要掌握下面兩個框架:

本文將主要介紹GoConvey框架的基本使用方法,從而指導讀者更好的進行測試實踐,最終寫出簡單優(yōu)雅的測試代碼。

GoConvey簡介

GoConvey是一款針對Golang的測試框架,可以管理和運行測試用例,同時提供了豐富的斷言函數(shù),并支持很多 Web 界面特性。
Golang雖然自帶了單元測試功能,并且在GoConvey框架誕生之前也出現(xiàn)了許多第三方測試框架,但沒有一個測試框架像GoConvey一樣能夠讓程序員如此簡潔優(yōu)雅的編寫測試代碼。

安裝

在命令行運行下面的命令:

go get github.com/smartystreets/goconvey

運行時間較長,運行完后你會發(fā)現(xiàn):

  1. 在$GOPATH/src目錄下新增了github.com子目錄,該子目錄里包含了GoConvey框架的庫代碼
  2. 在$GOPATH/bin目錄下新增了GoConvey框架的可執(zhí)行程序goconvey

注:上面是在gopath時代使用GoConvey的API前的安裝方法,而在gomod時代一般不需要先顯式安裝(gomod機制會自動從goproxy拉取依賴到本地cache),除非要使用GoConvey的web界面,這時需要提前安裝GoConvey的二進制,命令為go install github.com/smartystreets/goconvey@latest

基本使用方法

我們通過一個案例來介紹GoConvey框架的基本使用方法,并對要點進行歸納。

產品代碼

我們實現(xiàn)一個判斷兩個字符串切片是否相等的函數(shù)StringSliceEqual,主要邏輯包括:

  • 兩個字符串切片長度不相等時,返回false
  • 兩個字符串切片一個是nil,另一個不是nil時,返回false
  • 遍歷兩個切片,比較對應索引的兩個切片元素值,如果不相等,返回false
  • 否則,返回true

根據(jù)上面的邏輯,代碼實現(xiàn)如下所示:

func StringSliceEqual(a, b []string) bool {
    if len(a) != len(b) {
        return false
    }

    if (a == nil) != (b == nil) {
        return false
    }

    for i, v := range a {
        if v != b[i] {
            return false
        }
    }
    return true
}

對于邏輯“兩個字符串切片一個是nil,另一個不是nil時,返回false”的實現(xiàn)代碼有點不好理解:

if (a == nil) != (b == nil) {
    return false
}

我們實例化一下a和b,即[]string{}和[]string(nil),這時兩個字符串切片的長度都是0,但肯定不相等。

測試代碼

先寫一個正常情況的測試用例,如下所示:

import (
    "testing"
    . "github.com/smartystreets/goconvey/convey"
)

func TestStringSliceEqual(t *testing.T) {
    Convey("TestStringSliceEqual should return true when a != nil  && b != nil", t, func() {
        a := []string{"hello", "goconvey"}
        b := []string{"hello", "goconvey"}
        So(StringSliceEqual(a, b), ShouldBeTrue)
    })
}

由于GoConvey框架兼容Golang原生的單元測試,所以可以使用go test -v來運行測試。
打開命令行,進入$GOPATH/src/infra/alg目錄下,運行go test -v,則測試用例的執(zhí)行結果日下:

=== RUN   TestStringSliceEqual

  TestStringSliceEqual should return true when a != nil  && b != nil ?


1 total assertion

--- PASS: TestStringSliceEqual (0.00s)
PASS
ok      infra/alg       0.006s

上面的測試用例代碼有如下幾個要點:

  1. import goconvey包時,前面加點號".",以減少冗余的代碼。凡是在測試代碼中看到Convey和So兩個方法,肯定是convey包的,不要在產品代碼中定義相同的函數(shù)名
  2. 測試函數(shù)的名字必須以Test開頭,而且參數(shù)類型必須為*testing.T
  3. 每個測試用例必須使用Convey函數(shù)包裹起來,它的第一個參數(shù)為string類型的測試描述,第二個參數(shù)為測試函數(shù)的入?yún)ⅲ愋蜑?testing.T),第三個參數(shù)為不接收任何參數(shù)也不返回任何值的函數(shù)(習慣使用閉包)
  4. Convey函數(shù)的第三個參數(shù)閉包的實現(xiàn)中通過So函數(shù)完成斷言判斷,它的第一個參數(shù)為實際值,第二個參數(shù)為斷言函數(shù)變量,第三個參數(shù)或者沒有(當?shù)诙€參數(shù)為類ShouldBeTrue形式的函數(shù)變量)或者有(當?shù)诙€函數(shù)為類ShouldEqual形式的函數(shù)變量)

我們故意將該測試用例改為不過:

import (
    "testing"
    . "github.com/smartystreets/goconvey/convey"
)

func TestStringSliceEqual(t *testing.T) {
    Convey("TestStringSliceEqual should return true when a != nil  && b != nil", t, func() {
        a := []string{"hello", "goconvey"}
        b := []string{"hello", "goconvey"}
        So(StringSliceEqual(a, b), ShouldBeFalse)
    })
}

測試用例的執(zhí)行結果日下:

=== RUN   TestStringSliceEqual

  TestStringSliceEqual should return true when a != nil  && b != nil ?


Failures:

  * /Users/zhangxiaolong/Desktop/D/go-workspace/src/infra/alg/slice_test.go 
  Line 45:
  Expected: false
  Actual:   true


1 total assertion

--- FAIL: TestStringSliceEqual (0.00s)
FAIL
exit status 1
FAIL    infra/alg       0.006s


我們再補充3個測試用例:

import (
    "testing"
    . "github.com/smartystreets/goconvey/convey"
)

func TestStringSliceEqual(t *testing.T) {
    Convey("TestStringSliceEqual should return true when a != nil  && b != nil", t, func() {
        a := []string{"hello", "goconvey"}
        b := []string{"hello", "goconvey"}
        So(StringSliceEqual(a, b), ShouldBeTrue)
    })

    Convey("TestStringSliceEqual should return true when a == nil  && b == nil", t, func() {
        So(StringSliceEqual(nil, nil), ShouldBeTrue)
    })

    Convey("TestStringSliceEqual should return false when a == nil  && b != nil", t, func() {
        a := []string(nil)
        b := []string{}
        So(StringSliceEqual(a, b), ShouldBeFalse)
    })

    Convey("TestStringSliceEqual should return false when a != nil  && b != nil", t, func() {
        a := []string{"hello", "world"}
        b := []string{"hello", "goconvey"}
        So(StringSliceEqual(a, b), ShouldBeFalse)
    })
}

從上面的測試代碼可以看出,每一個Convey語句對應一個測試用例,那么一個函數(shù)的多個測試用例可以通過一個測試函數(shù)的多個Convey語句來呈現(xiàn)。

測試用例的執(zhí)行結果如下:

=== RUN   TestStringSliceEqual

  TestStringSliceEqual should return true when a != nil  && b != nil ?


1 total assertion


  TestStringSliceEqual should return true when a == nil  && b == nil ?


2 total assertions


  TestStringSliceEqual should return false when a == nil  && b != nil ?


3 total assertions


  TestStringSliceEqual should return false when a != nil  && b != nil ?


4 total assertions

--- PASS: TestStringSliceEqual (0.00s)
PASS
ok      infra/alg       0.006s

Convey語句的嵌套

Convey語句可以無限嵌套,以體現(xiàn)測試用例之間的關系。需要注意的是,只有最外層的Convey需要傳入*testing.T類型的變量t。
我們將前面的測試用例通過嵌套的方式寫另一個版本:

import (
    "testing"
    . "github.com/smartystreets/goconvey/convey"
)

func TestStringSliceEqual(t *testing.T) {
    Convey("TestStringSliceEqual", t, func() {
        Convey("should return true when a != nil  && b != nil", func() {
            a := []string{"hello", "goconvey"}
            b := []string{"hello", "goconvey"}
            So(StringSliceEqual(a, b), ShouldBeTrue)
        })

        Convey("should return true when a == nil  && b == nil", func() {
            So(StringSliceEqual(nil, nil), ShouldBeTrue)
        })

        Convey("should return false when a == nil  && b != nil", func() {
            a := []string(nil)
            b := []string{}
            So(StringSliceEqual(a, b), ShouldBeFalse)
        })

        Convey("should return false when a != nil  && b != nil", func() {
            a := []string{"hello", "world"}
            b := []string{"hello", "goconvey"}
            So(StringSliceEqual(a, b), ShouldBeFalse)
        })
    })
}

測試用例的執(zhí)行結果如下:

=== RUN   TestStringSliceEqual

  TestStringSliceEqual 
    should return true when a != nil  && b != nil ?
    should return true when a == nil  && b == nil ?
    should return false when a == nil  && b != nil ?
    should return false when a != nil  && b != nil ?


4 total assertions

--- PASS: TestStringSliceEqual (0.00s)
PASS
ok      infra/alg       0.006s

可見,Convey語句嵌套的測試日志和Convey語句不嵌套的測試日志的顯示有差異,筆者更喜歡這種以測試函數(shù)為單位多個測試用例集中顯示的形式。

此外,Convey語句嵌套還有一種三層嵌套的慣用法,即按BDD風格來寫測試用例,核心點是通過GWT(Given…When…Then)格式來描述測試用例,示例如下:

func TestStringSliceEqualIfBothNotNil(t *testing.T) {
    Convey("Given two string slice which are both not nil", t, func() {
        a := []string{"hello", "goconvey"}
        b := []string{"hello", "goconvey"}
        Convey("When the comparision is done", func() {
            result := StringSliceEqual(a, b)
            Convey("Then the result should be true", func() {
                So(result, ShouldBeTrue)
            })
        })
    })
}

GWT測試用例的執(zhí)行結果如下:

=== RUN   TestStringSliceEqualIfBothNotNil

  Given two string slice which are both not nil 
    When the comparision is done 
      Then the result should be true ?


1 total assertion

--- PASS: TestStringSliceEqualIfBothNotNil (0.00s)
ok      infra/alg       0.007s

按GWT格式寫測試用例時,每一組GWT對應一條測試用例,即最內層的Convey語句不像兩層嵌套時可以有多個,而是只能有一個Convey語句。

我們依次寫出其余三個用例的三層嵌套形式:

func TestStringSliceEqualIfBothNil(t *testing.T) {
    Convey("Given two string slice which are both nil", t, func() {
        var a []string = nil
        var b []string = nil
        Convey("When the comparision is done", func() {
            result := StringSliceEqual(a, b)
            Convey("Then the result should be true", func() {
                So(result, ShouldBeTrue)
            })
        })
    })
}

func TestStringSliceNotEqualIfNotBothNil(t *testing.T) {
    Convey("Given two string slice which are both nil", t, func() {
        a := []string(nil)
        b := []string{}
        Convey("When the comparision is done", func() {
            result := StringSliceEqual(a, b)
            Convey("Then the result should be false", func() {
                So(result, ShouldBeFalse)
            })
        })
    })
}

func TestStringSliceNotEqualIfBothNotNil(t *testing.T) {
    Convey("Given two string slice which are both not nil", t, func() {
        a := []string{"hello", "world"}
        b := []string{"hello", "goconvey"}
        Convey("When the comparision is done", func() {
            result := StringSliceEqual(a, b)
            Convey("Then the result should be false", func() {
                So(result, ShouldBeFalse)
            })
        })
    })
}

我們再將上面的四條用例使用測試套的形式來寫,即一個測試函數(shù)包含多條用例,每條用例使用Convey語句四層嵌套的慣用法:

func TestStringSliceEqual(t *testing.T) {
    Convey("TestStringSliceEqualIfBothNotNil", t, func() {
        Convey("Given two string slice which are both not nil", func() {
            a := []string{"hello", "goconvey"}
            b := []string{"hello", "goconvey"}
            Convey("When the comparision is done", func() {
                result := StringSliceEqual(a, b)
                Convey("Then the result should be true", func() {
                    So(result, ShouldBeTrue)
                })
            })
        })
    })

    Convey("TestStringSliceEqualIfBothNil", t, func() {
        Convey("Given two string slice which are both nil", func() {
            var a []string = nil
            var b []string = nil
            Convey("When the comparision is done", func() {
                result := StringSliceEqual(a, b)
                Convey("Then the result should be true", func() {
                    So(result, ShouldBeTrue)
                })
            })
        })
    })

    Convey("TestStringSliceNotEqualIfNotBothNil", t, func() {
        Convey("Given two string slice which are both nil", func() {
            a := []string(nil)
            b := []string{}
            Convey("When the comparision is done", func() {
                result := StringSliceEqual(a, b)
                Convey("Then the result should be false", func() {
                    So(result, ShouldBeFalse)
                })
            })
        })
    })

    Convey("TestStringSliceNotEqualIfBothNotNil", t, func() {
        Convey("Given two string slice which are both not nil", func() {
            a := []string{"hello", "world"}
            b := []string{"hello", "goconvey"}
            Convey("When the comparision is done", func() {
                result := StringSliceEqual(a, b)
                Convey("Then the result should be false", func() {
                    So(result, ShouldBeFalse)
                })
            })
        })
    })

}

Web 界面

GoConvey不僅支持在命令行進行自動化編譯測試,而且還支持在 Web 界面進行自動化編譯測試。想要使用GoConvey的 Web 界面特性,需要在測試文件所在目錄下執(zhí)行goconvey:

$GOPATH/bin/goconvey

這時彈出一個頁面,如下圖所示:

goconvey-web.png

在 Web 界面中:

  1. 可以設置界面主題
  2. 查看完整的測試結果
  3. 使用瀏覽器提醒等實用功能
  4. 自動檢測代碼變動并編譯測試
  5. 半自動化書寫測試用例
  6. 查看測試覆蓋率
  7. 臨時屏蔽某個包的編譯測試

Skip

針對想忽略但又不想刪掉或注釋掉某些斷言操作,GoConvey提供了Convey/So的Skip方法:

  • SkipConvey函數(shù)表明相應的閉包函數(shù)將不被執(zhí)行
  • SkipSo函數(shù)表明相應的斷言將不被執(zhí)行

當存在SkipConvey或SkipSo時,測試日志中會顯式打上"skipped"形式的標記:

  • 當測試代碼中存在SkipConvey時,相應閉包函數(shù)中不管是否為SkipSo,都將被忽略,測試日志中對應的符號僅為一個"?"
  • 當測試代碼Convey語句中存在SkipSo時,測試日志中每個So對應一個"?"或"?",每個SkipSo對應一個"?",按實際順序排列
  • 不管存在SkipConvey還是SkipSo時,測試日志中都有字符串"{n} total assertions (one or more sections skipped)",其中{n}表示測試中實際已運行的斷言語句數(shù)

定制斷言函數(shù)

我們先看一下So函數(shù)的聲明:

func So(actual interface{}, assert Assertion, expected ...interface{})

第二個參數(shù)為assert,是一個函數(shù)變量,它的類型Assertion的定義為:

type Assertion func(actual interface{}, expected ...interface{}) string

當Assertion的變量的返回值為""時表示斷言成功,否則表示失敗:

const assertionSuccess = ""

我們簡單實現(xiàn)一個Assertion函數(shù):

func ShouldSummerBeComming(actual interface{}, expected ...interface{}) string {
    if actual == "summer" && expected[0] == "comming" {
        return ""
    } else {
        return "summer is not comming!"
    }
}

我們仍然在slice_test文件中寫一個簡單測試:

func TestSummer(t *testing.T) {
    Convey("TestSummer", t, func() {
        So("summer", ShouldSummerBeComming, "comming")
        So("winter", ShouldSummerBeComming, "comming")
    })
}

根據(jù)ShouldSummerBeComming的實現(xiàn),Convey語句中第一個So將斷言成功,第二個So將斷言失敗。
我們運行測試,查看執(zhí)行結果,符合期望:

=== RUN   TestSummer

  TestSummer ??


Failures:

  * /Users/zhangxiaolong/Desktop/D/go-workspace/src/infra/alg/slice_test.go 
  Line 52:
  summer is not comming!


2 total assertions

--- FAIL: TestSummer (0.00s)
FAIL
exit status 1
FAIL    infra/alg       0.006s

小結

Golang雖然自帶了單元測試功能,但筆者建議讀者使用已經成熟的第三方測試框架。本文主要介紹了GoConvey框架,通過文字結合代碼示例講解基本的使用方法,要點歸納如下:

  1. import goconvey包時,前面加點號".",以減少冗余的代碼;
  2. 測試函數(shù)的名字必須以Test開頭,而且參數(shù)類型必須為*testing.T;
  3. 每個測試用例必須使用Convey語句包裹起來,推薦使用Convey語句的嵌套,即一個函數(shù)有一個或多個測試函數(shù),一個測試函數(shù)嵌套兩層、三層或四層Convey語句;
  4. Convey語句的第三個參數(shù)習慣以閉包的形式實現(xiàn),在閉包中通過So語句完成斷言;
  5. 使用GoConvey框架的 Web 界面特性,作為命令行的補充;
  6. 在適當?shù)膱鼍跋率褂肧kipConvey函數(shù)或SkipSo函數(shù);
  7. 當測試中有需要時,可以定制斷言函數(shù)。

至此,希望讀者已經掌握了GoConvey框架的基本用法,從而可以寫出簡單優(yōu)雅的測試代碼。

然而,事情并沒有這么簡單!試想,如果在被測函數(shù)中調用了底層rand包的Intn函數(shù),你會如何寫測試代碼?經過思考,你應該會發(fā)現(xiàn)需要給rand包的Intn函數(shù)打樁。如何低成本的滿足用戶各種測試場景的打樁訴求,這正是GoMonkey框架的專長。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容