☰
Go 語言單元測試與效能測試實戰:用 testing 套件與 go test 守護 Web 應用程式碼品質
2026/10/7 9:40:44 网站建设 项目流程
  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载

本篇文章以本書第 11 章「錯誤處理、除錯和測試」中的測試部分為核心,完整講解 Go 語言內建testing測試框架與go test命令的實戰用法,涵蓋單元測試的編寫規則、執行流程、結果判讀,以及以Benchmark為代表的壓力/效能測試方法。讀完本文後,你將能為自己的 Go 套件(尤其是 Web 應用中的核心邏輯)建立一套可重複執行的單元測試與效能基準測試,讓每次程式碼修改後都能透過一行go test快速完成回歸驗證。

為什麼需要單元測試與效能測試

開發程式其中很重要的一點是測試:我們如何保證程式碼的品質,如何保證每個函式是可執行的、執行結果是正確的,又如何保證寫出來的程式碼效能是好的?單元測試的重點在於發現程式設計或實現上的邏輯錯誤,使問題及早暴露,便於問題的定位與解決;而效能測試(壓力測試)的重點則在於發現程式設計上的瓶頸,讓上線的程式在高併發情境下依然保持穩定。

Go 語言自帶一個輕量級的測試框架testing,配合內建的go test命令即可實現單元測試與效能測試。testing框架與其他語言中的測試框架類似,你可以基於這個框架編寫針對相應函式的測試案例,也可以基於該框架編寫壓力測試案例。此外,社群還提供了 gotests 外掛用於自動產生測試程式碼,可透過以下命令安裝:

go get -u -v github.com/cweill/gotests/...

安裝後即可利用它從既有函式簽名自動生成_test.go骨架,降低手寫樣板程式碼的成本。

如何編寫單元測試案例

go test命令只能在相應的目錄下執行該目錄內的所有測試檔案,因此我們先建立一個專案目錄gotest,讓所有的程式碼與測試程式碼都放在同一個目錄下。

步驟一:建立被測套件 gotest.go

在gotest目錄下建立第一個檔案gotest.go,其中宣告了套件名稱gotest,並提供一個執行除法運算的函式:

package gotest import ( "errors" ) func Division(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("除數不能為 0") } return a / b, nil }

這個函式設計體現了本書 11.1 錯誤處理 中介紹的 Go 錯誤處理準則:凡是可能出錯的 API 都回傳一個error變數,呼叫方透過把回傳的error與nil比較來判定操作是否成功。這裡的errors.New("除數不能為 0")正是標準套件errors提供的錯誤構造方式——它把字串包裝成一個實現了內建error介面(type error interface { Error() string })的物件。這樣設計的好處是:除法函式在除數為 0 的邊界情況下不會產生Inf或NaN這類難以察覺的結果,而是明確地返回錯誤,讓測試與上層呼叫都能夠可靠地感知失敗。

步驟二:建立測試檔案 gotest_test.go

gotest_test.go是我們的單元測試檔案。編寫 Go 測試檔案時必須記住以下原則:

  • 檔名必須以_test.go結尾,這樣執行go test時才會執行到相應的程式碼;
  • 必須 importtesting這個套件;
  • 所有的測試案例函式必須以Test開頭;
  • 測試案例會按照原始碼中書寫的順序依次執行;
  • 測試函式TestXxx(t *testing.T)的參數是testing.T,我們可以使用該型別來記錄錯誤或測試狀態;
  • 測試格式為func TestXxx(t *testing.T),其中Xxx部分可以是任意的字母數字組合,但首字母不能是小寫字母 [a-z],例如Testintdiv是錯誤的函式名稱;
  • 在測試函式中,透過呼叫testing.T的Error、Errorf、FailNow、Fatal等方法來標記測試不通過,呼叫Log方法記錄測試資訊。

下面是本書提供的測試案例程式碼:

package gotest import ( "testing" ) func Test_Division_1(t *testing.T) { if i, e := Division(6, 2); i != 3 || e != nil { // try a unit test on function t.Error("除法函式測試沒通過") // 如果不是如預期的那麼就報錯 } else { t.Log("第一個測試通過了") //記錄一些你期望記錄的資訊 } } func Test_Division_2(t *testing.T) { t.Error("就是不通過") }

這裡的Test_Division_1屬於典型的「表驅動思維」雛形:先執行被測函式Division(6, 2),再同時斷言商值與錯誤兩個回傳值,只要任一條件不符就呼叫t.Error讓測試失敗。Test_Division_2則故意寫死失敗,用來示範測試失敗時的輸出樣式。

步驟三:執行 go test 並判讀結果

在專案目錄下執行go test,會顯示如下資訊:

--- FAIL: Test_Division_2 (0.00 seconds) gotest_test.go:16: 就是不通過 FAIL exit status 1 FAIL gotest 0.013s

從結果可以看出測試沒有通過,因為第二個測試函式中寫死了t.Error。那麼第一個函式的執行情況如何呢?預設情況下執行go test不會顯示測試通過的資訊,我們需要帶上參數go test -v(verbose,詳細模式),就會顯示如下資訊:

=== RUN Test_Division_1 --- PASS: Test_Division_1 (0.00 seconds) gotest_test.go:11: 第一個測試通過了 === RUN Test_Division_2 --- FAIL: Test_Division_2 (0.00 seconds) gotest_test.go:16: 就是不通過 FAIL exit status 1 FAIL gotest 0.012s

上面的輸出詳細展示了測試的過程:測試函式 1Test_Division_1測試通過,測試函式 2Test_Division_2測試失敗,最後得出整體測試不通過的結論。注意-v模式輸出的幾個關鍵元素:=== RUN標記測試開始、--- PASS/FAIL給出單個案例的結果、括號內的0.00 seconds是該案例的執行耗時,而失敗資訊會附帶gotest_test.go:16這樣的「檔案:行號」定位,方便我們直接跳到出錯位置。

步驟四:修正失敗的測試案例

接下來把測試函式 2 修改成真正驗證邊界條件的測試——檢查除數為 0 時是否如預期返回錯誤:

func Test_Division_2(t *testing.T) { if _, e := Division(6, 0); e == nil { // try a unit test on function t.Error("Division did not work as expected.") // 如果不是如預期的那麼就報錯 } else { t.Log("one test passed.", e) //記錄一些你期望記錄的資訊 } }

然後執行go test -v,顯示如下資訊,測試通過了:

=== RUN Test_Division_1 --- PASS: Test_Division_1 (0.00 seconds) gotest_test.go:11: 第一個測試通過了 === RUN Test_Division_2 --- PASS: Test_Division_2 (0.00 seconds) gotest_test.go:20: one test passed. 除數不能為 0 PASS ok gotest 0.013s

這次兩個測試案例全部PASS,最終輸出ok gotest 0.013s,代表整個套件測試通過且總耗時約 13 毫秒。Test_Division_2現在覆蓋了Division函式的錯誤分支——除數為 0 時必須返回非 nil 的 error,這與gotest.go中errors.New("除數不能為 0")的實現互相印證,形成完整的「正常路徑 + 錯誤路徑」雙分支覆蓋。

testing.T 的常用方法速查

在實際編寫測試時,testing.T提供的核心方法各有分工,本書提到的與常見的用法整理如下:

方法作用特點
t.Log/t.Logf記錄測試資訊僅在-v模式或測試失敗時輸出
t.Error/t.Errorf標記測試失敗並記錄訊息記錄後繼續執行後續程式碼
t.Fatal/t.Fatalf標記測試失敗並記錄訊息呼叫後立即中止當前測試
t.FailNow標記測試失敗並立即中止不輸出訊息,通常配合其他記錄方法使用
t.Skip跳過當前測試常用於依賴外部資源(如資料庫)暫時不可用的場景

選擇Error還是Fatal的原則很簡單:如果該失敗點之後的斷言仍有意義就繼續執行(用Error),否則(例如初始化失敗、被測物件為 nil)就立即停止(用Fatal),避免後續空指標恐慌淹沒真正的錯誤資訊。

如何編寫壓力測試(效能測試)

壓力測試用來檢測函式(方法)的效能,與編寫單元功能測試的方法類似,此處不再贅述,但需要注意以下幾點:

  • 壓力測試案例必須遵循如下格式,其中XXX可以是任意字母數字的組合,但首字母不能是小寫字母:
func BenchmarkXXX(b *testing.B) { ... }
  • go test不會預設執行壓力測試的函式;如果要執行壓力測試,需要帶上參數-test.bench,語法為-test.bench="test_name_regex",例如go test -test.bench=".*"表示測試全部壓力測試函式;
  • 在壓力測試案例中,請記得在迴圈體內使用testing.B.N,使測試可以正常執行——框架會根據設定的時間預算自動調整N的取值,迴圈必須以b.N為上限才能得到可信的基準資料;
  • 檔名也必須以_test.go結尾。

下面我們建立一個壓力測試檔案webbench_test.go,程式碼如下:

package gotest import ( "testing" ) func Benchmark_Division(b *testing.B) { for i := 0; i < b.N; i++ { //use b.N for looping Division(4, 5) } } func Benchmark_TimeConsumingFunction(b *testing.B) { b.StopTimer() //呼叫該函式停止壓力測試的時間計數 //做一些初始化的工作,例如讀取檔案資料,資料庫連線之類的, //這樣這些時間不影響我們測試函式本身的效能 b.StartTimer() //重新開始時間 for i := 0; i < b.N; i++ { Division(4, 5) } }

b.StopTimer 與 b.StartTimer 的意義

Benchmark_TimeConsumingFunction示範了一個重要的基準測試技巧:如果測試函式之前需要做昂貴的初始化(例如讀取檔案、建立資料庫連線、載入測試資料),這些初始化耗時會污染基準結果,讓效能資料失真。解法就是像上面的程式碼一樣,在初始化前呼叫b.StopTimer()暫停計時,完成初始化後再呼叫b.StartTimer()恢復計時,再進入以b.N為上限的測量迴圈。這樣最後輸出的ns/op就是函式本身的純執行成本。

執行壓力測試並解讀結果

執行命令go test webbench_test.go -test.bench=".*",可以看到如下結果:

Benchmark_Division-4 500000000 7.76 ns/op 456 B/op 14 allocs/op Benchmark_TimeConsumingFunction-4 500000000 7.80 ns/op 224 B/op 4 allocs/op PASS ok gotest 9.364s

上面的結果顯示我們沒有執行任何TestXXX的單元測試函式,只執行了壓力測試函式。第一條顯示Benchmark_Division執行了 500000000 次(5 億次),每次執行的平均時間是 7.76 納秒,每次執行配置 456 B/op、14 allocs/op;第二條顯示Benchmark_TimeConsumingFunction執行了 500000000 次,每次平均執行時間是 7.80 納秒,每次配置 224 B/op、4 allocs/op。最後一行顯示測試總共的執行時間 9.364 秒。

對照兩組資料可以發現:Benchmark_TimeConsumingFunction透過StopTimer/StartTimer把初始化成本隔離在計時之外後,其每次執行的記憶體配置次數(allocs/op)從 14 次降到 4 次——這正是效能測試的價值所在:它能量化每次呼叫的耗時與記憶體開銷,幫我們定位優化前後的差異。在 Web 應用場景中,Handler 的每次請求處理都伴隨記憶體配置,追蹤allocs/op對降低 GC 壓力、提升高併發下的吞吐量非常關鍵。

若需要在基準測試中同時輸出記憶體分配統計,可在函式開頭呼叫b.ReportAllocs();若被測操作本身極快、單次測量誤差大,也可考慮在迴圈內手動累加多次操作後再交給b.N調控,這都是testing.B提供的標準擴充手法。

從本書第 11 章的脈絡看測試的定位

本篇文章對應於本書第 11 章「錯誤處理、除錯和測試」的 11.3 小節。該章節的完整脈絡是:11.1 錯誤處理 講解error型別設計與錯誤處理模式,11.2 使用 GDB 除錯 講解執行期除錯,本小節則負責測試——三者共同構成「寫出高品質程式碼」的閉環:錯誤處理讓失敗可見、GDB 讓問題可定位、測試讓回歸可預防。正如 11.4 小結 所述:一個好的 Web 應用必定有良好的錯誤處理機制、良好的單元測試與壓力測試,以保證上線之後程式碼能夠保持良好效能並按預期運行。

本書在 code 目錄 中收錄了各章節的完整 Go 原始碼範例(例如ch.2.x的基礎語法、ch.5.x的資料庫與 NoSQL 存取等),這些範例與本小節的測試方法是互補的:你可以把本節的gotest.go/gotest_test.go模式套用到任何一個真實的套件上,為其核心函式補上單元測試與基準測試。需要強調的是,go test不僅僅是開發期工具——每次修改程式碼後執行一次go test,就能低成本地完成回歸測試,這是維持 Web 專案長期可維護性的基本紀律。

小結

透過上面對單元測試和壓力測試的學習,我們可以看到testing套件非常輕量,編寫單元測試和壓力測試案例非常簡單,配合內建的go test命令就可以非常方便地進行測試。這樣在我們每次修改完程式碼,執行一下go test就可以簡單地完成回歸測試。

延伸閱讀

  • 目錄
  • 上一節:使用 GDB 除錯
  • 下一節:小結
  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载
上一篇:如何用Summarize实现视频内容搜索?幻灯片文字提取与关键词定位
下一篇:解锁显卡潜能:NVIDIA Profile Inspector完全指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询