本文在實習期間完成并完善,無任何公司機密,僅做語言交流學習之用。
持續更新。
1.Golang的單元測試
Go語言提供了豐富的單測功能。在Go中,我們通常認為函數是最小的可執行單元。本例中使用兩個簡單的函數:IsOdd和IsPalindrome來進行Go單測的研究。

在VSCode中,在函數名上點擊右鍵,選擇“Go: Generate Unit Tests For Function"即可生成單測文件。

前往對應的*_test.go,開始以表格化的方式填寫測試用例。這里我們每個函數都填5個測試用例:

這里name代表的是測試用例的名字,這里建議每個測試用例的名字都唯一,否則你很有可能不知道發生錯誤的用例到底是哪個。同時我在34行和38行加入了當測試用例不通過時,輸出測試用例名字的語句。這樣就可以快速定位到測試不通過的測試用例。這里我們故意將第5個測試用例的want寫錯(32767是奇數,所以本應該返回true的,也就是want應該為true),看看測試工具是怎么報錯的。
點擊函數名上面那個run test,,這樣可以開始執行測試。run test只會運行它所下面一行所聲明的測試函數。如果需要測試所有函數可以命令行輸入go test或者go test -v。對IsOdd的測試如下:

可以看到其實不自己添加用例輸出語句,FAIL的時候go的測試工具也會幫你輸出到底是哪個用例不通過。所以34行和38行的代碼并不是必須的。我們把第五個測試用例修改成正確的,再次運行測試:

這個時候全部的測試用例就通過了,而且因為這是一種完全只是關心輸入輸出的測試,并不涉及到函數內部的具體細節,我們稱之為“黑盒測試”。
而其中我們還能選擇不同的日志等級輸出錯誤信息,單測框架提供的所有日志方法都會結束測試,若只想標記測試用例不通過,請使用t.Fail()。具體的日志方法如表:
方法功能
t.Log()/t.Logf() 打印標準等級日志,同時結束測試
t.Error()/t.Errorf() 打印錯誤等級日志,同時結束測試
t.Fatal()/t.Fatalf() 打印致命等級日志,同時結束測試
2.Golang的測試覆蓋率
Go的測試覆蓋率一般指的是測試用例可以觸發函數內的多少個分支語句占全部的分支語句的比例,在VSCode中可以以顏色區分的方式來判斷當前的測試函數覆蓋到了哪些分支,沒有覆蓋到哪些分支。首先,在測試文件中,右鍵任意一個測試函數,選擇"Go: Toggle Test Coverage In Current Package"來開始進行測試樣例覆蓋。

這里我們將返回值應為false的測試用例給去掉,執行測試。測試完成后回到被測試的函數源文件中,可以發現被測到的分支為以墨綠色標記,而沒有被測試到的代碼分支以紅色標記。

將測試樣例復原后再進行測試,可以發現測試覆蓋率達到了100%,同時所有的代碼都是墨綠色的了:

3.Golang的基準性能測試
3.1 非并行Golang benchmark
Golang提供了測試函數運行性能的工具,對于所有的函數來說,其性能測試函數都是在前面加Benchmark。我們還是用上面所說的兩個函數,來寫一下它們的benchmark測試函數(但是我沒找到怎么一鍵生成benchmark的選項):

其中b.ReportAllocs()會報告這個函數的內存使用情況(執行一次方法要申請多少次內存,每次申請需要申請多大的內存),也可以通過指定-benchmem參數來輸出所有函數的內存性能。
執行測試命令(或者使用VSCode的那個run benchmark按鈕測試單個函數的benchmark):
Linux:
go test -bench=.
go test -bench=. -benchmem # If memory analysis info is required
go test -bench=. -benchtime=3s # If benchmark test time is not 1s, use -benchtime to set it
Windows:
go test -bench="."
go test -bench="." -benchmem # If memory analysis info is required
go test -bench="." -benchtime=3s # If benchmark test time is not 1s, use -benchtime to set it
測試出來的結果如下:

輸出解讀:
數據意義
BenchmarkIsOdd-8 以P=8來測試IsOdd的性能
232279942 代表在1s內(如果沒有指定-benchtime則默認測試時間為1s)執行了IsOdd 232279942次
5.16 ns/op 代表每執行一次IsOdd所花費的時間為5.16 ns
0B/op 代表每執行一次IsOdd所分配的內存為0B
0 allocs/op 代表每執行一次IsOdd申請分配內存的次數為0次
我們新寫一個函數,這個函數涉及到分配內存。我們先寫一個AllocFixedArray來申請一個長度固定的數組并循環往里面填寫數據,然后再寫一個AllocMutableArray來申請一個長度可擴充的數組,使這兩個申請數組的長度相同,觀察它們的性能:

首先對它們做單測,確保代碼運行上沒有問題,由于這里沒有邏輯判斷,所以有一個測試用例就夠了:

確認結果正確后,寫出它們對應的Benchmark函數:

然后可以點擊run benchmark一個個測,或者直接全部函數都測一下,這里選擇全部測試:

可以發現,使用make聲明固定長度的內存是沒有allocs的,而append底部會在數組長度不足的時候對數組進行擴充,所以會有內存的申請。并且我們可以看到,append因為底層申請了內存,性能大大下降,AllocMutableArray的執行時間是AllocFixedArray的差不多25倍。這個也提示我們,盡量要對數組的大小有一個預先的估計,并申請好一個capacity比較接近最大上限的數組。
3.2 并行Golang benchmark
測試的時候同樣可以使用并行的方法去并發測試指定的時間內能執行多少次該方法,其基本語法為:
b
我們試試將執行比較慢的AllocMutableArray()來并發處理,看看會如何:

執行基準測試,得到結果:

我們可以發現,在P=8并發執行AllocMutableArray之后,執行時間從73.6ns/op降到了14.6ns/op。
3.3 Golang benckmark中的計時器
假設說一個函數在執行之前,要先執行一些外部的初始化操作。而我們如果在go test里面制定了-benchtime選項,它記錄的將會是整個Benchmark函數的運行時間。所以我們需要有一種操縱定時器的方法,來獲得整個服務精確的運行時間。假設我們的IsOdd,它在執行之前需要睡眠100毫秒,那么我們就可以在執行完睡眠之后,使用重置計數器的方法開始計時。


OK,測試用的總共時間為3.166s。然后我們加上不對初始化進行計時的代碼(取消16、18行的注釋),重新測一次:

可以看到測試時間有顯著的下降,這說明使用b.StopTimer()后,沒有將初始化的時間算在總測試時間內。
方法功能
b.StartTimer() 復原或打開計時器,當Benchmark執行前會首先執行b.StartTimer()
b.StopTimer() 暫停當前計時器
b.ResetTimer() 重置當前計時器的值,go官方說該函數在計時器運行時無效,但我試了一下是有效的。建議先b.StopTimer()后再調用此方法,最后再b.StartTimer()
StartTimer, StopTimer和ResetTimer其實就相當于我們常用的秒表的三個按鈕:開始,暫停和復位。當Benchmark函數執行之前,就會自動調用StartTimer。而ResetTimer函數生效的前提是必須先調用StopTimer。通過這樣的控制,就可以控制基準測試的計時器,防止一些無關部分的時間被測算進來了。
3.4 Golang benchmark的Profile(性能分析)
golang的benchmark提供了一種輸出性能分析的工具,在測試benchmark命令的前提下,加上參數即可,下面提供了三種獲取全部基準測試函數不同性能的指令:
test -bench
當獲得這些性能文件之后,也會相應地留下一個***.test為文件名的可執行文件。為什么要留下這個測試時候生成的臨時程序呢?在生成profile文件的時候,為了減少冗余,生成的文件全部都是不含符號信息的,也就是說其實并沒有記錄性能條目對應的是哪個函數的性能,所以需要有一個這樣的副本程序來記錄符號信息。
當我們獲得這些文件之后,使用go自帶的pprof來查看這些文件所表示的含義,其中-nodecount=10表示僅顯示前10個最耗性能的條目:
=
其中***為根據實際需要所替換的字符串,一個名為go-learning的程序的cpu占用情況分析如下圖:

可以看到,IsPalindrome的占用時間排第2位,僅次于gc。所以我們可以著手去從這個函數進行優化。
4.Golang的Example測試(樣例測試)
樣例測試比較像平時在一些算法刷題平臺(比如LeetCode)的題目的一些例子,樣例測試以Example打頭,其邏輯也很簡單,就是使用fmt.Println輸出該測試用例的返回結果,然后在函數體的末尾使用如圖的注釋,一一對應每個fmt.Println的輸出:

17行的output首字母大寫小寫均可。
如果13~16行輸出的結果和18~21行的結果相對應,go test就會PASS,否則就會FAIL,并打印出實際輸出和期望輸出。
5.Go的Mock方法
5.1 Mock的簡介
mock,中文譯名為“模仿,假的”,顧名思義就是構建一個模擬對象,來替換掉一些需要在特定環境下觸發的服務,使其可以在不修改原服務的前提下達到測試的目的。本文介紹一種是基于gomonkey的函數/變量Mock方法。
5.2 基于gomonkey的函數Mock方法
在使用gomonkey之前,我們要先安裝它,輸入命令:
go get github.com/agiledragon/gomonkey
并在開頭import該包:
import
假設我們有一個函數IsRest,當調用這個函數的時候,程序會判斷一下現在的時間是否已經是下午5點之后,如果是,就返回nil,表示現在是下班時間了。否則返回非nil值,表示現在還沒到下班時間。我們先寫出這個函數:

那我們測試的時候肯定不可能等到5點再去測這個函數吧?否則這測試不就沒法做了。這個時候我們先生成它的單測函數,然后施加mock:

其中,108行~114行是對IsRest進行mock的方法,ApplyFunc指的是對函數進行Mock,第二個參數就是要使用的Mock方法。
那我們來執行測試:

說明在執行測試用例的時候,gomonkey成功地把IsRest方法給mock掉了。
6. 總結
Go語言本身提供了豐富的單元測試和性能測試方法,但是在提供Mock方法上還是略有不足。本文從Gomock, Gomonkey和GoStub出發,總結了一些創建Mock對象的方法。如果對于Go測試有進一步興趣的,可以去了解GoConvey,GoMonkey,GoStub和GoMock的教程,下面列出了一個作者寫的關于這四個測試工具的文章,供讀者參考:
GoConvey框架使用指南
https://www.jianshu.com/p/633b55d73ddd?www.jianshu.comGoMock框架使用指南
https://www.jianshu.com/p/f4e773a1b11f?www.jianshu.comGoStub框架使用指南
https://www.jianshu.com/p/70a93a9ed186?www.jianshu.comMonkey框架使用指南
Monkey框架使用指南?www.jianshu.com