GoConvey架構使用指南

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

序言

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

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

  • GoConvey
  • GoStub
  • GoMock

筆者將通過多篇文章來闡述這三個測試架構,同時對於GoStub架構還將進行二次開發實踐,以便高效的解決較複雜情境的打樁問題。

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

說明:本文的實踐都是在 Mac OS X 下進行,操作過程可能與讀者所在的系統略有差異。

GoConvey簡介

GoConvey類似於C/C++語言的測試架構GTest,是一款針對Golang的測試架構,可以管理和運行測試案例,同時提供了豐富的斷言函數,並支援很多 Web 介面特性。
Golang雖然內建了單元測試功能,並且在GoConvey架構誕生之前也出現了許多第三方測試架構,但沒有一個測試架構像GoConvey一樣能夠讓程式員如此簡潔優雅的編寫測試代碼。

安裝

在命令列輸入命令:

sudo go get github.com/smartystreets/goconvey

你會發現:

  1. 在$GOPATH/src目錄下新增了github.com子目錄,子目錄裡包含了GoConvey架構的庫代碼。
  2. 在/usr/local目錄下產生了子目錄go,同時在go目錄下包含了三個子目錄,分別是bin,pkg和src。

我們將/usr/local/go/bin拷貝到$GOPATH下:

cp -r bin $GOPATH

基本使用方法

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

產品代碼

我們實現一個判斷兩個字串切片是否相等的函數StringSliceEqual,主要邏輯包括:

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

根據上面的邏輯,代碼實現如下所示:

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”的實現代碼有點不好理解:

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,則測試案例的執行結果日下:

=== RUN   TestStringSliceEqual  TestStringSliceEqual should return true when a != nil  && b != nil 1 total assertion--- PASS: TestStringSliceEqual (0.00s)PASSok      infra/alg       0.006s

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

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

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

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

測試案例的執行結果日下:

=== 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:   true1 total assertion--- FAIL: TestStringSliceEqual (0.00s)FAILexit status 1FAIL    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語句對應一個測試案例,那麼一個函數的多個測試案例可以通過一個測試函數的多個Convey語句來呈現。

測試案例的執行結果如下:

=== 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)PASSok      infra/alg       0.006s

Convey語句的嵌套

Convey語句可以無限嵌套,以體現測試案例之間的關係。需要注意的是,只有最外層的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)        })    })}

測試案例的執行結果如下:

=== 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)PASSok      infra/alg       0.006s

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

Web 介面

GoConvey不僅支援在命令列進行自動化編譯測試,而且還支援在 Web 介面進行自動化編譯測試。想要使用GoConvey的 Web 介面特性,需要在測試檔案所在目錄下執行goconvey:

$GOPATH/bin/goconvey

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


goconvey-web.png

在 Web 介面中:

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

Skip

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

  • SkipConvey函數表明相應的閉包函數將不被執行
  • SkipSo函數表明相應的斷言將不被執行

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

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

定製斷言函數

我們先看一下So的函數原型:

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

第二個參數為assertion,它的原型為:

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

當assertion的傳回值為""時表示斷言成功,否則表示失敗,GoConvey架構中的相關代碼為:

const (    success                = ""    needExactValues        = "This assertion requires exactly %d comparison values (you provided %d)."    needNonEmptyCollection = "This assertion requires at least 1 comparison value (you provided 0).")

我們簡單實現一個assertion函數:

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")    })}

根據ShouldSummerBeComming的實現,Convey語句中第一個So將斷言成功,第二個So將宣告失敗。
我們運行測試,查看執行結果,符合期望:

=== 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)FAILexit status 1FAIL    infra/alg       0.006s

小結

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

  1. import goconvey包時,前面加點號".",以減少冗餘的代碼
  2. 測試函數的名字必須以Test開頭,而且參數類型必須為*testing.T
  3. 每個測試案例必須使用Convey函數包裹起來,推薦使用Convey語句的嵌套,即一個函數有一個測試函數,測試函數中嵌套兩級Convey語句,第一級Convey語句對應測試函數,第二級Convey語句對應測試案例
  4. Convey語句的第三個參數習慣以閉包的形式實現,在閉包中通過So陳述式完成斷言
  5. 使用GoConvey架構的 Web 介面特性,作為命令列的補充
  6. 在適當的情境下使用SkipConvey函數或SkipSo函數
  7. 當測試中有需要時,可以定製斷言函數

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

然而,事情並沒有這麼簡單!試想,如果函數中多次調用了底層的同步操作,比如os包的exec函數,我們該如何寫測試代碼?
其實答案也並不複雜,我們將在下一篇文章中揭曉。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.