使用 Go Convey 做 BDD 測試的入門指南

前面在「Go 代碼測試時怎麼打樁?給大家寫了幾個常用案例」中我們介紹了在單元測試中使用gomonkey爲代碼進行打樁的各種方法。

今天我們介紹在 Go 單元測試中另外一個很好用的工具庫goconvey,上面說的gomonkey屬於在 Test Double 方面提供能力,也就是我們通常說的mock,用它們可以自定義一套實現來替換項目中的代碼實現。

goconvey則是一個幫助我們組織和管理測試用例的框架,提供了ConveySo兩種方法來搭配使用,支持樹形結構方便構造各種場景。它本身是不會提供 mock 能力的,你可以基於goconvey來組織你的單測,在需要mock的場景下與gomonkey配合使用。

goconvey 的安裝和基本用法

在項目中使用 goconvey 前需要先在項目依賴中添加 goconvey,安裝命令如下:

go get github.com/smartystreets/goconvey

我們先看一下 goconvey 官方給出的使用示例。

package package_name

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

func TestSpec(t *testing.T) {

// Only pass t into top-level Convey calls
 Convey("Given some integer with a starting value", t, func() {
  x := 1
  Convey("When the integer is incremented", func() {
   x++
   Convey("The value should be greater by one", func() {
    So(x, ShouldEqual, 2)
   })
  })
 })
}

通過這個例子,正好說一下在使用 goconvy 的過程中需要注意的幾個點:

goconvey爲我們提供了很多種ShouldXXX類斷言方法在So()函數中使用,來比對前後兩個參數之間的關係。

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

另外如果斷言失敗,goconvey底層會調用t.Fail()方法來告訴Go,你的go test就會失敗,所以如果使用了goconvey,就不用在代碼裏手動調用t.Fail()了。

goconvey 實戰演示

TestMain 設置

首先需要在測試的入口 TestMain 中要加上SuppressConsoleStatisticsPrintConsoleStatistics,用於在測試完成後輸出測試結果。

func TestMain(m *testing.M) {
 // convey在TestMain場景下的入口
 SuppressConsoleStatistics()
 result := m.Run()
 // convey在TestMain場景下的結果打印
 PrintConsoleStatistics()
 os.Exit(result)
}

BDD 行爲驅動測試實戰

下面我們使用 goconvey 爲 util 包的工具函數 PasswordComplexityVerify 編寫測試,PasswordComplexityVerify 的功能是用來檢查用戶註冊賬號時輸入的密碼是否滿足複雜密碼的要求。

package util
func PasswordComplexityVerify(s string) bool {
 var (
  hasMinLen  = false
  hasUpper   = false
  hasLower   = false
  hasNumber  = false
  hasSpecial = false
 )
 ......
 return hasMinLen && hasUpper && hasLower && hasNumber && hasSpecial
}

使用 Convey 爲他編寫的測試如下:

func TestPasswordComplexityVerify(t *testing.T) {
 Convey("Given a simple password", t, func() {
  password := "123456"
  Convey("When run it for password complexity checking", func() {
   result := util.PasswordComplexityVerify(password)
   Convey("Then the checking result should be false", func() {
    So(result, ShouldBeFalse)
   })
  })
 })

 Convey("Given a complex password", t, func() {
  password := "123@1~356Wrx"
  Convey("When run it for password complexity checking", func() {
   result := util.PasswordComplexityVerify(password)
   Convey("Then the checking result should be true", func() {
    So(result, ShouldBeTrue)
   })
  })
 })
}

這裏我們不僅僅有嵌套的 Convey,還有並列的 Convey。通過這種關係來表達各個不同測試之間的關聯關係。在兩個並列 Convey 中我們分別進行了正向和負向測試。

你可能問了,寫單元測試就寫唄,咋還冒出來個正向測試、負向測試呢?其實它們非常好理解:

結合我們在description參數中的描述,我們就可以建立起來類似BDD(行爲驅動測試)的語義:

BDD 測試中的描述信息通常使用的是 Given、When、Then 引導的狀語從句,如果喜歡用中文寫描述信息也要記得使用類似語境的句子。

你可能會問這麼寫了有什麼用,咱們用命令來看看測試運行的效果,我們可以看到輸出的測試結果會按照單測中 Convey 書寫的層級,分層級顯示。

GoConvey 提供的斷言方法

goconvey爲我們提供了很多種ShouldXXX類斷言方法在So()函數中使用,來比對前後兩個參數之間的關係,主要有下面幾類,大家用到的時候可以來這裏參考。

一般相等類
So(thing1, ShouldEqual, thing2)
So(thing1, ShouldNotEqual, thing2)
So(thing1, ShouldResemble, thing2) // 用於數組、切片、map和結構體相等
So(thing1, ShouldNotResemble, thing2)
So(thing1, ShouldPointTo, thing2)
So(thing1, ShouldNotPointTo, thing2)
So(thing1, ShouldBeNil)
So(thing1, ShouldNotBeNil)
So(thing1, ShouldBeTrue)
So(thing1, ShouldBeFalse)
So(thing1, ShouldBeZeroValue)
數字數量比較類
So(1, ShouldBeGreaterThan, 0)
So(1, ShouldBeGreaterThanOrEqualTo, 0)
So(1, ShouldBeLessThan, 2)
So(1, ShouldBeLessThanOrEqualTo, 2)
So(1.1, ShouldBeBetween, .8, 1.2)
So(1.1, ShouldNotBeBetween, 2, 3)
So(1.1, ShouldBeBetweenOrEqual, .9, 1.1)
So(1.1, ShouldNotBeBetweenOrEqual, 1000, 2000)
So(1.0, ShouldAlmostEqual, 0.99999999, .0001)   // tolerance is optional; default 0.0000000001
So(1.0, ShouldNotAlmostEqual, 0.9, .0001)
包含類
So([]int{2, 4, 6}, ShouldContain, 4)
So([]int{2, 4, 6}, ShouldNotContain, 5)
So(4, ShouldBeIn, ...[]int{2, 4, 6})
So(4, ShouldNotBeIn, ...[]int{1, 3, 5})
So([]int{}, ShouldBeEmpty)
So([]int{1}, ShouldNotBeEmpty)
So(map[string]string{"a": "b"}, ShouldContainKey, "a")
So(map[string]string{"a": "b"}, ShouldNotContainKey, "b")
So(map[string]string{"a": "b"}, ShouldNotBeEmpty)
So(map[string]string{}, ShouldBeEmpty)
So(map[string]string{"a": "b"}, ShouldHaveLength, 1) // supports map, slice, chan, and string
字符串類
So("asdf", ShouldStartWith, "as")
So("asdf", ShouldNotStartWith, "df")
So("asdf", ShouldEndWith, "df")
So("asdf", ShouldNotEndWith, "df")
So("asdf", ShouldContainSubstring, "稍等一下") // optional 'expected occurences' arguments?
So("asdf", ShouldNotContainSubstring, "er")
So("adsf", ShouldBeBlank)
So("asdf", ShouldNotBeBlank)
panic 類
So(func(), ShouldPanic)
So(func(), ShouldNotPanic)
So(func(), ShouldPanicWith, "") // or errors.New("something")
So(func(), ShouldNotPanicWith, "") // or errors.New("something")
類型檢查類
So(1, ShouldHaveSameTypeAs, 0)
So(1, ShouldNotHaveSameTypeAs, "asdf")
時間和時間間隔類
So(time.Now(), ShouldHappenBefore, time.Now())
So(time.Now(), ShouldHappenOnOrBefore, time.Now())
So(time.Now(), ShouldHappenAfter, time.Now())
So(time.Now(), ShouldHappenOnOrAfter, time.Now())
So(time.Now(), ShouldHappenBetween, time.Now(), time.Now())
So(time.Now(), ShouldHappenOnOrBetween, time.Now(), time.Now())
So(time.Now(), ShouldNotHappenOnOrBetween, time.Now(), time.Now())
So(time.Now(), ShouldHappenWithin, duration, time.Now())
So(time.Now(), ShouldNotHappenWithin, duration, time.Now())
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/uvuxvBN5BMOfgzhgYdPj7g