這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
6.程式測試和文檔
6.1程式測試
Go語言中提供了 go test 命令,它不僅僅可以對程式碼封裝進行測試,還可以對個別源碼檔案進行測試,只要存在針對這些測試的測試源碼檔案。除此之外,Go語言還在標準庫中提供了一個專門用於測試的程式碼封裝 testing,它提供了編寫測試源碼檔案所需的一切。
1.功能測試
測試源碼檔案總應該與被它測試的源碼檔案處於同一程式碼封裝內。在編寫測試源碼檔案的時候,總是會用到標準庫程式碼封裝 testing 中的 API。testing 包為Go語言的程式碼封裝提供了自動化測試支援。它的目標是與 go test 命令協同使用,以自動執行目標程式碼封裝中的任何測試函數。
(1).編寫功能測試函數
在測試源碼檔案中,針對其他源碼檔案中的程式實體的功能測試程式總是以函數為單位的。被用於測試程式實體功能的函數的名稱和簽名形如:
func TestXxx(t *testing.T)
其中,Xxx 應該是大寫字母開頭的若干字母或數位組合,通常情況下會將 Xxx 替換成被測試的程式實體的名稱。可以利用 *testing.T 類型的參數 t 上的一些方法對功能測試的過程進行記錄和控制。使用t的值上的方法記錄的資訊會在測試結束之後(不論成敗)一併列印到標準輸出上。
(2)常規記錄
參數 t 上的 Log 和 Logf 方法一般用於記錄一些常規資訊,以展示測試程式的運行過程以及被測試程式實體的即時狀態。調用語句如下:
t.Log("Tomorrow is a ", "good ", "day ")//類似於fmt.Printlnt.Logf("Tomorrow is a %s", " good day ")//類似於fmt.Printf
使用 go test –v 命令,兩者都會列印如下資訊:
xxx_test.go:10: Tomorrow is a good day
xxx_test.go:11: Tomorrow is a good day
xxx_test.go:10: 代表了調用語句所在的測試源碼檔案的名稱以及出現的行號。
(3).錯誤記錄
參數 t 上的 Error 和 Errorf 方法被用於錯誤資訊。當被測試的程式實體的狀態不正確的時候,使用 t.Error 或 t.Errorf 方法,及時對當前的錯誤狀態進行記錄。例如:
actLen := len(s)if actLen != expLen { t.Errorf("Error: The length of slice should be %d but %d.\n", expLen, actLen)}
調用 t.Error 方法相當於先後對 t.Log 和 t.Fail 方法進行調用,而調用 t.Errorf 方法則相當於與先後對 t.Logf 和 t.Fail 方法進行調用。
(4).致命錯誤記錄
參數 t 上的 Fatal 和 Fatalf 方法被用於記錄致命的被程式實體的狀態錯誤。所謂致命錯誤是指使得測試無法繼續進行的錯誤。例如:
if listener == nil { t.Fatalf("Listener startup failing! (addr=%s)!\n", serverAddr)}
調用 t.Fatal 方法相當於先後對 t.Log 和 t.FailNow 方法進行調用,而調用 t.Fatalf 方法則相當於先後對 t.Logf 和 t.FailNow 方法進行調用。
(5).失敗標記
如果需要標記當前測試函數中的測試是失敗的,那麼就需要用到 t.Fail 方法。對 t.Fail 方法的調用不會終止當前測試函數的執行。但是,此函數的測試結果已經被標記為失敗了。
(6).立即失敗標記
方法 t.FailNow 與 t.Fail 不同的地方是,它在被調用時會立即終止當前測試函數的執行。這會使得當前的測試回合程式轉而去執行其他的測試函數。
注意:只能在運行測試函數的 Goroutine 中調用 t.FailNow 方法,而不能在測試代碼建立出的 Goroutine 中調用它。不過,在其他的 Goroutine 中調用 t.FailNow 方法也不會造成什麼錯誤,只是它不會產生任何效果而已。
(7).失敗判斷
在調用 t.Failed 方法之後,會獲得一個 bool 類型的結果值,它代表了當前的測試函數中的測試是否已被標記為失敗。
(8).忽略測試
調用 t.SkipNow 方法目的是標記當前測試函數為已經被忽略,並且立即終止該函數的執行,當前的測試回合程式會轉而去執行其他測試函數。與 t.FailNow 方法相同,t.SkipNow 方法也只能在運行測試函數的 Goroutine 中被調用。
調用 t.Skip 方法相當於先後對 t.Log 和 t.SkipNow 方法進行調用,而調用 t.Skipf 方法則相當於先後對 t.Logf 和 t.SkipNow 方法進行調用。
方法 t.Skipped 的結果值會告知當前的測試是否已被忽略。
(9).並行運行
方法 t.Parallel 的調用會使當前的測試函數被標記為可並行啟動並執行。這會使測試回合程式可以並發地執行它以及其他可並行啟動並執行測試函數。
(10).功能測試的運行
使用 goc2p 項目的程式碼封裝 cnet/ctcp 和 pkgtool 為例,如下是下載地址:
https://github.com/hyper-carrot/go_command_tutorial
該程式碼封裝中僅包含一個名為 tcp_test.go 的測試源碼檔案。該測試源碼檔案包含了兩個測試函數。一個是名為 TestPrimeFuncs 的功能測試函數,一個是名為 BenchmarkPrimeFuncs 的基準測試函數。
使用 go test 命令運行 cnet/ctcp 包中的測試結果如下:
如果只想運行程式碼封裝中部分測試的話,有兩種方式可以選擇:
第一種就是 go test 命令後面以測試源碼檔案及其測試的源碼檔案為參數,而不是程式碼封裝。例如:
go test envir_test.go envir.go
第二種方式是使用標記 -run 。-run 標記的值應該為一個Regex。名稱與此Regex匹配的功能測試函數,才會在當次的測試回合過程中被執行。運行如下:
該程式碼封裝的測試源碼檔案 tcp_test.go 中的功能測試函數 TestPrimeFuncs 會被執行。但當Regex改為“Prima”後,由於沒有 cnet/ctcp 包並沒有名稱與之匹配的功能測試函數。例如:
在Go語言中,可以通過方法 t.Log 和 t.Logf 來記錄測試過程。但是,在預設情況下,使用此方法列印的資訊不會被顯示出來的。因此,需要標記 -v , -v 作用是在測試回合結束後列印出所有在測試過程中被記錄的日誌。出於測試的考慮,強烈建議在測試源碼檔案中使用方法參數t的值上的方法來記錄日誌。
再看如下的一條樣本,同時測試程式碼封裝 cnet/ctcp 和程式碼封裝 pkgtool,如下運行:
(11).關於測試回合的時間
現在考慮這樣一種測試情境,在一個測試函數包含一段了耗時較長的代碼,並且需要嚴格規定執行這個測試函數的耗時上限。可以在執行 go test 命令時加入標記 -timeout,且在達到其值所代表的時間上限時測試還未結束,那麼就會引發一個運行時恐慌。-timeout 標記的值是類型 time.Duration 可以接受的時間標記法。例如,1h20s代表1小時20秒,2h45m 代表2小時45分鐘,200ms代表200毫秒。
有效時間單位
時間單位 |
字串標記法 |
納秒 |
“ns” |
微秒 |
“us”或“µs” |
毫秒 |
“ms” |
秒 |
“s” |
分鐘 |
“m” |
小時 |
“h” |
之前運行程式碼封裝 cnet/ctcp 中的功能測試函數的執行耗時大約2秒左右。現在通過 -timeout 標記將測試耗時上限設定為100毫秒,並運行測試。如下:
E:\Software\Go\goc2p\src>go test -timeout 100ms cnet/ctcp
panic: test timed out after 100ms
……
FAIL cnet/ctcp 0.715s
如果只是想讓測試儘快結束,使用 -short 標記意味著之後要啟動並執行測試盡量縮短它們的已耗用時間。程式碼封裝 testing 中有一個名為 Short 的函數。這個函數在被調用後會返回一個類型 bool 的值。這個值表明了是否在執行 go test 命令的時候加入了 -short 標記。如果這個函數返回的 bool 值為 true ,那麼就可以根據具體情況,去剪裁測試代碼從而縮短測試回合時間了。可以在一個功能測試函數中寫一段類似的代碼:
if testing.Short() { multiSend(serverAddr, "SenderT", 1, (2 * time.Second), showLog)} else { multiSend(serverAddr, "SenderT1", 2, (2 * time.Second), showLog) multiSend(serverAddr, "SenderT2", 1, (2 * time.Second), showLog)}
這段代碼來自測試源碼檔案 tcp_test.go 中的測試函數 TestPrimeFuncs,但做了修改,關注點放在了函數 multiSend 上,根據 testing.Short() 的返回的結果值做了不同的策略。
(12).測試的並發執行
如果功能測試運行在擁有多核CPU或者多CPU的電腦上,那麼可以使用並發的方式來執行測試。通過 -parallel 標記,能夠設定允許並發執行的功能測試函數的最大數量。但能夠成為被並發執行的功能測試函數需要具備一個先決條件:在功能測試函數的開始處加入代碼 t.Parallel() 。在調用 t.Parallel 方法的時候,執行功能測試函數的測試回合程式會阻塞在這裡,並等待其他同樣滿足並發執行條件的測試函數。當所有需要並存執行的測試函數都被清點且阻塞後,命令程式會根據 -parallel 標記的值,全部或者部分地並發執行這些功能測試函數中的在語句 t.Parallel() 之後的那些代碼。
-parallel 標記的預設值是通過標準庫的程式碼封裝 runtime 的函數 GOMAXPROCS 設定的值。該函數的作用是設定Go語言並發處理的最大數量。實際上,即使 -parallel 標記的值大於這個Go語言最大並發處理數,真正能夠並發執行的功能測試函數的數量也不會比它多,所以在通常情況下,並不需要在命令中加入 -parallel 標記,讓它的實際值為預設值就好了。但需要注意的是,Go語言最大並發處理數的預設值為 1 。如果想要某些測試函數中的代碼被並發地執行,要做的就是在測試源碼檔案的 init 函數中設定適當的Go語言最大並發處理數,並在這些測試函數中加入語句 t.Parallel()。
2.基準測試
所謂基準測試(Benchmark Test,簡稱BMT)是指,通過一些科學的手段實現對一類測試對象的某項效能指標進行可測量、可重複和可比對的測試。很多時候,基準測試已被狹義地稱為效能測試。
(1).編寫基準測試函數
與功能測試相同,針對其他源碼檔案中的程式實體的基準測試程式也是以測試函數為單位的。一個基準測試函數的名稱和簽名如下:
func BenchmarkXxx(b *testing.B)
(2).關於計時器
在 *testing.B 類型中,與定時器相關的方法有3個它們是 StartTimer、StopTimer 和 ResetTimer 。這3個方法被用於操縱基準測試函數的計時器。該計時器的作用是計算當前基準測試函數的執行時間。
調用 b.StartTimer 方法意味著開始對當前的測試函數的執行進行計時。它總會在開始執行基準測試函數的時候被自動地調用。這個方法被暴露出來的意義在於:計時器在被停止之後重新啟動。調用 b.StopTimer 方法可以使當前測試函數的計時器停止。例如:
package bmtimport ( "testing" "time")func Benchmark(b *testing.B) { customTimerTag := false if customTimerTag { b.StopTimer() } b.SetBytes(12345678) time.Sleep(time.Second) if customTimerTag { b.StartTimer() }}
如上檔案命名為 bmt_test.go 存放到工作區的 testing/bmt 程式碼封裝中,運行基準測試如下:
現在將其中的 customTimerTag 變數的值改為 true ,再來運行測試,如下:
從上面兩個運行可以看見最後兩行的輸出內容不同。在第二個中,倒數第二行的3個部分分別代表了當前測試函數的名稱,操作次數以及操作平均耗時。其中,操作次數是當前的基準測試函數被執行的次數,而操作平均耗時是當前基準測試函數的平均執行時間。
同樣觀察兩個啟動並執行倒數第二行可知,當 customTimerTag 為 true 時,基準測試函數 Benchmark 可以被執行多次,而當 customTimerTag 為 false 時,它往往只能獲得一次執行機會。這些都是由於 testing 包中有這樣的一個限制:在基準測試函數單次執行時間超過指定值(預設為1秒,也可以由標記 -benchtime 自訂)的情況下,只執行該基準測試函數一次。也就是測試回合程式會在不超過這個執行時間上限的情況下儘可能多次地執行一個基準測試函數。
當 customTimerTag 為 true 時,在調用語句 time.Sleep(time.Second) 的之前和之後,分別停止和重啟了 Benchmark 函數的計時器,這就相當於不把 time.Sleep(time.Second) 語句的執行時間算在 Benchmark 函數的執行時間之內,執行 Benchmark 函數的時間已經基本可以忽略不計了(可以從 0.00 ns/op 可知),這樣測試回合程式在 Benchmark 函數的累積執行時間為到達時間上限之前就會連續不斷地重複執行它。
當 customTimerTag 為 false 時,調用語句 time.Sleep(time.Second) 讓當前的測試程式“休息”1 秒,Benchmark 函數的單次執行時間就肯定會大於 1 秒。因此測試回合程式就不會對 Benchmark 函數執行第二次。
對於方法 b.ResetTimer 在被調用時,會重設當前基準測試函數的計時器,就是把該函數的執行時間重設為 0,這相當於把當前函數中在 b.ResetTimer 語句之前的所有語句的執行時間都從該函數的執行時間中減去。
(3).關於記憶體配置統計
方法 b.ReportAllocs 的含義是判斷在啟動當前測試的 go test 命令的後面是否有 -benchmem 標記。它會返回一個 bool 類型的結果值。
方法 b.SetBytes 接受一個 int64 類型的值,它被用於記錄在單次操作中被處理的位元組的數量。
當 customTimerTag 為 false,運行中,針對 Benchmark 函數的操作資訊的那一行資訊中多處了一個部分—— 12.34MB/s 。它的含義是每秒被處理的位元組的數量(以 MB 為單位)。這個數量其實等於測試回合程式在執行(可能是多次) Benchmark 函數的過程中每秒調用 b.SetBytes 方法的次數乘以傳入的那個整數。
首先試想一個情境:在基準測試函數 Benchmark 中測試的是一個向檔案系統中寫入資料的函數。在寫入成功後,會調用 b.SetBytes 方法並把真正寫入的位元組數作為參數傳入。通過測試結果資訊中的 xxx MB/s ,可以獲知該函數每秒能向檔案系統寫入多少MB( MB )的資料了。
從上面總結,b.SetBytes 方法能夠從輸入輸出(IO)的角度統計出被測試的程式實體的實際效能。
(4).基準測試的運行
在上面的測試中,go test 命令只運行了 cnet/ctcp 包中的功能測試。下面說說 go test 命令的基準測試標記說明。
標記名稱 |
標記描述 |
-bench regexp |
在預設情況下,go test命令不會運行任何基準測試,但可以使用該標記以執行匹配“regexp”處的 Regex的基準測試函數,“regexp”可以被替換成任何Regex。如果需要運行所有的基準測試函數, 添加 –bench . 或 –bench=. 或 –bench=“.” |
-benchmem |
在輸出內容中包含基準測試的記憶體配置統計資訊 |
-benchtime t |
用來間接地控制單個基準測試函數的操作次數。這裡的“t”指的是執行單個測試函數的累積耗時上限。 “t”處的內容使用的是類型time.Duration可接受的時間標記法。“t”的預設值是1s |
運行針對程式碼封裝 cnet/ctcp 運行基準測試的如下:
結構體類型 testing.B 的欄位 N 可以被用來設定對基準測試函數中的某一個代碼塊的重複執行次數。例如:
for i := 0; I < b.N; i++ { //測試代碼}
運行如下:
對於計算 N 的值的具體演算法,可以查看標準庫的 testing 包的源碼檔案 benchmark.go 中的相關代碼。
如果要看到基準測試函數的操作次數和操作平均耗時的同時獲得這個過程中的記憶體配置情況,就需要用到 -benchmem 標記。例如下面:
“23416 B/op”是每次操作分配的位元組的平均數為23416個。“109 allocs/op”是每次操作分配記憶體的次數平均為109次。
go test命令還可以接受一個可自訂測試回合次數並在測試回合期間改變Go語言最大並發處理數的標記 -cpu , -cpu 標記可以是一個整數列表,多個整數之間用逗號分隔。-cpu 標記的處理方式和 -parallel 標記相反,-parallel 標記預設使用Go語言最大並發處理數,而 -cpu 標記卻會直接設定它。但是,由 -cpu 標記引發的Go語言最大並發處理數的設定作業並不會影響 -parallel 標記的預設值。因為 -parallel 標記的值是在測試回合程式初始化的時候設定的。如果在 go test 命令中沒有顯式地加入 -parallel 標記,則它的值會被設定為測試回合程式初始化時刻的Go語言最大並發處理數。在這個時刻,測試程式運行還沒有把 -cpu 標記的值(如果有的話)解析成整數數組,也就無法使用這個數組中的整數設定Go語言最大並發處理數了。
使用 -cpu 標記運行如下:
測試回合程式執行基準測試函數 BenchmarkPrimeFuncs 的次數是 7,這與 -cpu 標記的值1,2,4,8,12,16,20中的 7 個數字相對應。上面只是展示了基準測試的運行記錄,同樣這邊也調用了 7 次功能測試函數 TestPrimeFuncs 。
如上運行中,倒數 行的末尾包含了一行運行時環境資訊:[GOMAXPROCS=20, NUM_CPU=4, NUM_GOROUTINE=2],對於第一個 GOMAXPROCS 代表Go語言最大並發處理數,此處為 20 , “NUM_CPU”代表當前電腦的CPU總核心數,此處為 4,“NUM_GOROUTINE”代表當前時刻的並發程式的數量,此處為 2。
與並發處理有關的標記
標記名稱 |
使用樣本 |
說明 |
-parallel |
-parallel 4 |
功能:設定可並發執行的功能測試函數的最大數量 預設值:調用runtime.GOMAXPROCS(0)後的結果,即Go語言最大並發處理數量 先決條件:功能測試函數需要在開始處調用結構體testing.T類型的參數值的Parallel方法 生效的測試:功能測試 |
-cpu |
-cpu 1,2,4 |
功能:根據標記的值,迭代的設定Go語言並發處理最大數並執行全部功能測試或全部基準測試。 迭代的次數與標記值中的整數個數一致 預設值:“”,即Null 字元串 先決條件:無 生效的測試:功能測試和基準測試 |
注意: -cpu 和 -parallel 標記的範圍都是程式碼封裝,它們只能用於控制某一個程式碼封裝內的測試的流程。如果使用 go test 命令啟動了多個程式碼封裝的測試,那麼每個程式碼封裝中的功能測試永遠是可並發執行的,而基準測試永遠是串列執行的。如果把針對某一個程式碼封裝的所有測試的運行過程看成一個整體的話,若在執行 go test 命令時加入了 -bench 標記,則針對各個程式碼封裝的測試回合過程會被串列地執行,否則它們將被並發地執行。但無論如何,列印測試記錄和結果資訊的動作是嚴格按照 go test 命令後面的程式碼封裝從左往右的順序執行。
本篇講解了Go語言程式測試的功能測試和基準測試,下篇繼續講解有關Go語言程式測試的有關知識。
最後附上國內的Go語言社區(每篇更新一個)
Golangtc.com: 該社區是眾多的Go語言中文社區中比較活躍的一個。我們可以從中獲知很多Go語言方面的資訊。網址:http://www.golangtc.com