單元測試和基準測試

本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!html

在平時,經過代碼實戰部分你能夠看到,在寫每一個功能的時候,都會編寫測試代碼。那是由於 TDD(Test-Driven Development,測試驅動開發)中提倡先編寫測試代碼,而後再編寫功能代碼,每作一個修改後,都要執行一次單元測試和基準測試,以此來驗證功能和性能是否有問題。前端

特別是業務系統代碼常常變動,單元測試和基準測試也就顯得很是重要。而每種語言都有本身的測試框架,好比 Go 語言,它是門注重工程效率的語言,有着很是強大的工具鏈,它自帶的測試框架,能知足咱們大部分測試要求。算法

因此,這裏介紹如何使用 Go 測試框架作性能測試中的單元測試和基準測試後端

單元測試

Go 測試框架中支持白盒測試和黑盒測試。如今我就以 xx.go 這個文件爲例,給你詳細介紹下如何作單元測試。數組

整體步驟

總的來講,用 Go 測試框架作單元測試主要有這幾個步驟。瀏覽器

  • 第一,Go 測試框架要求測試代碼文件名以 _test.go 結尾。爲了測試 cache.go,咱們須要在 infrastructure/stores 目錄下建立一個 cache_test.go 文件。markdown

  • 第二,cache_test.go 中第一行若是是 package stores,則表示該測試是白盒測試,這意味着除了這個包的全局函數外,你還能夠測試它的私有函數;若是是 package stores_test,則表示黑盒測試,你只能夠測試全局函數,裏面的具體實現對於你來講是個黑盒子。併發

  • 第三,Go 測試框架要求單元測試函數須要以 Test 開頭。爲了測試 IntCache 和 ObjCache,咱們須要實現 TestIntCache 和 TestObjCache 這兩個函數,它們的參數類型都是 testing.T 指針。app

  • 第四,在測試過程當中,若是發現錯誤,能夠經過測試框架的 Error 方法或者 Fatal 方法輸出錯誤。不一樣的是,Error 方法僅僅輸出錯誤,而 Fatal 方法卻會結束當前測試。框架

  • 第五,在終端進入項目根目錄下,執行 go test ./infrastructure/stores 命令,將會執行 infrastructure/stores 目錄下的全部單元測試。

結果一般會有三列:

  1. 第一列是測試結果,ok 表示成功,FAIL 表示失敗;

  2. 第二列是被測試包的完整路徑;

  3. 第三列是執行測試耗費的時間。

當測試失敗時,輸出結果還會告訴你在哪一行報錯了,格式一般是:第一行是測試函數,第二行是文件名和該文件的第幾行,後面再跟上具體錯誤日誌。好比,TestIntCache 這個測試函數在 cache_test.go 文件第 19 行報錯。

覆蓋率

在單元測試中,除了測試功能是否正常外,還有個很重要的指標:覆蓋率。覆蓋率用來衡量單元測試覆蓋了多少代碼,它是由覆蓋測試統計出來的。覆蓋率越高,說明單元測試越完備,對代碼質量更有保障。

Go 的覆蓋測試使用很簡單,在 go test 命令後加上 -cover 參數便可。如執行 go test -cover ./infrastructure/stores 將會在以前的輸出結果後面加上 coverage 開頭的日誌。好比 coverage: 61.0% of statements 。

雖然覆蓋測試工具用起來很簡單,但要想將覆蓋率提高到 100% 則是很是困難。

第一,你須要找出來哪些地方沒有覆蓋到。

這裏咱們須要用到 -coverprofile 參數,將詳細的結果輸出到文件中。好比 go test -coverprofile cover.out 即是將覆蓋測試的結果輸出到 cover.out 中。而後咱們用命令 go tool cover -html=cover.out -o cover.html 將測試結果輸出爲 html 文件,再用瀏覽器打開即可以看到哪些代碼被單元測試覆蓋到了,哪些沒有被覆蓋到。其中綠色部分表示已覆蓋到的代碼,紅色部分表示沒有覆蓋到。效果以下:

圖片.png

咱們能夠看到,Add 方法有大片代碼沒被測試覆蓋到,咱們能夠經過修改測試代碼來覆蓋這部分代碼的測試。

第二,你須要對代碼邏輯很是熟悉,特別是熟悉代碼中的各類邊界條件,並在測試代碼中構造出這些邊界條件來測試。

假如代碼裏有三個條件分支 A、B、C,你須要將這三個條件都構造出來,才能覆蓋到它們各自分支下面的代碼。這意味着,你在編寫測試代碼的時候,須要編寫大量的測試用例,也就是構造大量的能觸發邊界條件的參數。若是沒有好的代碼功底,你的測試代碼容易由於大量參數而變得混亂、臃腫。

有時候,一個功能可能會有多個參數,每一個參數又有多個邊界值。當咱們採用控制變量法來測試的時候,全部參數的邊界值又會產生多種組合,這個數量一般是每一個參數邊界值數量的乘積。好比 A 參數有 2 個邊界值,B 參數有 3 個邊界值,那麼將會存在 6 種組合 。爲了便於管理,咱們須要將這些參數提取到核心測試代碼以外,用數組和循環來管理測試用例。

好比咱們在測試 IntCache 的 Add 方法時,按照 key、delta 這兩個參數的邊界狀況,就有 key 存在、不存在兩種狀況與 delta 爲 -一、0、1 這三種狀況的組合。咱們將 Add 方法的 key、delta 參數以及指望返回值的配置,用一個結構體數組來管理,並用循環遍歷數組,根據數組中每一個元素的值調用 Add 方法並判斷返回值與指望值是否相等。若是不相等則報錯,輸出有問題的測試用例,並終止測試。咱們能夠獲得以下代碼:

func TestIntCache_Add(t *testing.T) {

   cache := NewIntCache()

   cases := []struct {

      key    string

      delta  int64

      expect int64

   }{

      {"test1", 0, 0},

      {"test1", 1, 1},

      {"test1", -1, 0},

      {"test1", 0, 0},

      {"test2", 1, 1},

      {"test3", -1, -1},

   }

   for _, c := range cases {

      if cache.Add(c.key, c.delta) != c.expect {

         t.Fatal(c)

      }

   }

}
複製代碼

再次執行覆蓋測試,你將看到代碼覆蓋率從 61.0% 提高到了 76.3%,Add 方法只剩下一行代碼沒覆蓋到,而這行代碼須要經過構造併發條件才能覆蓋到。效果以下:

圖片.png

基準測試

基準測試屬於性能測試,一般用於對具體的功能函數作性能分析,好比加密算法函數。基準測試須要有對比測試,以便衡量不一樣代碼實現之間的性能差別,從中選取性能最好的實現方式。

在 Go 測試框架中,基準測試函數須要以 Benchmark 開頭。好比 BenchmarkIntCache_Set 表示對 IntCache 的 Set 方法進行基準測試。爲了與 sync.Map 和 ObjCache 對比,咱們還須要實現 BenchmarkObjCache_Set 和 BenchmarkSyncMap_Set 這兩個函數。

不過,當咱們要測試對象的方法比較多時,爲每一個對象的每一個方法都實現一個獨立的測試函數並非很方便。對此,咱們一般咱們採用分組測試的方式。Go 測試框架提供了一個 Run 方法可用於執行分組中的子測試,它有兩個參數,第一個是子測試的名稱,第二個是測試函數。

這裏,咱們能夠實現一個 BenchmarkCache_Set 函數來做爲 Set 方法的測試組入口,裏面包含 intCache、objCache、syncMap 這三個子測試。這樣咱們能夠爲每一個子測試統一初始化公共資源,並複用核心代碼邏輯。

爲了複用代碼,我實現了一個 benchmarkCacheSet 函數。它有三個參數:

  • 測試框架生成的 testing.B 對象指針;

  • setter 函數,它有 key 和 val 這兩個參數,幫助設置被測對象中的 KV 值;

  • 字符串數組 keys,表示用於測試的 key 集合。

在函數中,咱們調用框架的 ReportAllocs 方法,用於輸出內存分配信息。在循環開始前,調用框架的 StartTimer 開始計時,循環結束後調用 StopTimer 結束計時。

在 BenchmarkCache_Set 中先初始化 keys,而後在三個子測試中分別初始化 IntCache、ObjCache、sync.Map,生成各自的 setter,並調用 benchmarkCacheSet 函數。

最終,咱們的測試代碼以下:

func benchmarkCacheSet(b *testing.B, setter func(key string, val int64), keys []string) {

   b.ReportAllocs()

   b.StartTimer()

   l := len(keys)

   for i := 0; i < b.N; i++ {

      setter(keys[i%l], int64(i))

   }

   b.StopTimer()

}

func BenchmarkCache_Set(b *testing.B) {

   keys := make([]string, b.N, b.N)

   for i := 0; i < b.N; i++ {

      keys[i] = strconv.Itoa(i)

   }

   b.ResetTimer()

   b.Run("intCache", func(b *testing.B) {

      c := NewIntCache()

      setter := func(key string, val int64) {

         c.Set(key, val)

      }

      benchmarkCacheSet(b, setter, keys)

   })

   b.Run("objCache", func(b *testing.B) {

      c := NewObjCache()

      setter := func(key string, val int64) {

         c.Set(key, val)

      }

      benchmarkCacheSet(b, setter, keys)

   })

   b.Run("syncMap", func(b *testing.B) {

      c := sync.Map{}

      setter := func(key string, val int64) {

         c.Store(key, val)

      }

      benchmarkCacheSet(b, setter, keys)

   })

}
複製代碼

咱們能夠在終端執行命令 go test -bench=BenchmarkCache_Set ./infrastructure/stores 來運行上面實現的測試代碼,你能夠看到以 BenchmarkCache_Set 開頭的三個子測試的結果。

須要注意的是,每一個子測試後面都有一個 -8,表示使用了 8 個 CPU 核。

另外,Go 測試框架默認會使用全部的 CPU 核,但因爲電腦上一般會運行其餘的程序,使用全部的核可能會致使不一樣程序之間搶奪資源,影響測試結果。基於這一點,咱們能夠在參數中指定 CPU 核數來測試。

具體來講,咱們能夠經過 -cpu 參數指定多種 CPU 核數來測試性能。好比,執行命令 go test -bench=BenchmarkCache_Set -cpu 1,2,3 ./infrastructure/stores 後,咱們將看到每一個子測試都用了不一樣 CPU 核數來測試。不過,因爲咱們的測試代碼並無用到併發,所以測試結果不受 CPU 核數的影響。

相關文章
相關標籤/搜索