網站首頁 編程語言 正文
引言
本節帶你學習本專欄的第四模塊:工程管理?,F在項目的開發都不是一個人可以完成的,需要多人進行協作,那么在多人協作中如何保證代碼的質量,你寫的代碼如何被其他人使用,如何優化代碼的性能等, 就是第四模塊的內容。
這一講首先來學習 Go 語言的單元測試和基準測試。
單元測試
在開發完一個功能后,你可能會直接把代碼合并到代碼庫,用于上線或供其他人使用。但這樣是不對的,因為你還沒有對所寫的代碼進行測試。沒有經過測試的代碼邏輯可能會存在問題:如果強行合并到代碼庫,可能影響其他人的開發;如果強行上線,可能導致線上 Bug、影響用戶使用。
什么是單元測試
顧名思義,單元測試強調的是對單元進行測試。在開發中,一個單元可以是一個函數、一個模塊等。一般情況下,你要測試的單元應該是一個完整的最小單元,比如 Go 語言的函數。這樣的話,當每個最小單元都被驗證通過,那么整個模塊、甚至整個程序就都可以被驗證通過。
單元測試由開發者自己編寫,也就是誰改動了代碼,誰就要編寫相應的單元測試代碼以驗證本次改動的正確性。
Go 語言的單元測試
雖然每種編程語言里單元測試的概念是一樣的,但它們對單元測試的設計不一樣。Go 語言也有自己的單元測試規范,下面我會通過一個完整的示例為你講解,這個例子就是經典的斐波那契數列。
斐波那契數列是一個經典的黃金分隔數列:它的第 0 項是 0;第 1 項是 1;從第 2 項開始,每一項都等于前兩項之和。所以它的數列是:0、1、1、2、3、5、8、13、21……
說明:為了便于總結后面的函數方程式,我這里特意寫的從第 0 項開始,其實現實中沒有第 0 項。
根據以上規律,可以總結出它的函數方程式。
F(0)=0
F(1)=1
F(n)=F(n - 1)+F(n - 2)
有了函數方程式,再編寫一個 Go 語言函數來計算斐波那契數列就比較簡單了,代碼如下:
ch18/main.go
func Fibonacci(n int) int { if n < 0 { return 0 } if n == 0 { return 0 } if n == 1 { return 1 } return Fibonacci(n-1) + Fibonacci(n-2) }
也就是通過遞歸的方式實現了斐波那契數列的計算。
Fibonacci 函數已經編寫好了,可以供其他開發者使用,不過在使用之前,需要先對它進行單元測試。你需要新建一個 go 文件用于存放單元測試代碼。剛剛編寫的 Fibonacci 函數在ch18/main.go文件中,那么對 Fibonacci 函數進行單元測試的代碼需要放在ch18/main_test.go中*,*測試代碼如下:
ch18/main_test.go
func TestFibonacci(t *testing.T) { //預先定義的一組斐波那契數列作為測試用例 fsMap := map[int]int{} fsMap[0] = 0 fsMap[1] = 1 fsMap[2] = 1 fsMap[3] = 2 fsMap[4] = 3 fsMap[5] = 5 fsMap[6] = 8 fsMap[7] = 13 fsMap[8] = 21 fsMap[9] = 34 for k, v := range fsMap { fib := Fibonacci(k) if v == fib { t.Logf("結果正確:n為%d,值為%d", k, fib) } else { t.Errorf("結果錯誤:期望%d,但是計算的值是%d", v, fib) } } }
在這個單元測試中,我通過 map 預定義了一組測試用例,然后通過 Fibonacci 函數計算結果。同預定義的結果進行比較,如果相等,則說明 Fibonacci 函數計算正確,不相等則說明計算錯誤。
然后即可運行如下命令,進行單元測試:
? go test -v ./ch18
這行命令會運行 ch18 目錄下的所有單元測試,因為我只寫了一個單元測試,所以可以看到結果如下所示:
? go test -v ./ch18?
=== RUN ? TestFibonacci
? ? main_test.go:21: 結果正確:n為0,值為0
? ? main_test.go:21: 結果正確:n為1,值為1
? ? main_test.go:21: 結果正確:n為6,值為8
? ? main_test.go:21: 結果正確:n為8,值為21
? ? main_test.go:21: 結果正確:n為9,值為34
? ? main_test.go:21: 結果正確:n為2,值為1
? ? main_test.go:21: 結果正確:n為3,值為2
? ? main_test.go:21: 結果正確:n為4,值為3
? ? main_test.go:21: 結果正確:n為5,值為5
? ? main_test.go:21: 結果正確:n為7,值為13
--- PASS: TestFibonacci (0.00s)
PASS
ok ? ? ?gotour/ch18 ? ? (cached)
在打印的測試結果中,你可以看到 PASS 標記,說明單元測試通過,而且還可以看到我在單元測試中寫的日志。
這就是一個完整的 Go 語言單元測試用例,它是在 Go 語言提供的測試框架下完成的。Go 語言測試框架可以讓我們很容易地進行單元測試,但是需要遵循五點規則。
- 含有單元測試代碼的 go 文件必須以 _test.go 結尾,Go 語言測試工具只認符合這個規則的文件。
- 單元測試文件名 _test.go 前面的部分最好是被測試的函數所在的 go 文件的文件名,比如以上示例中單元測試文件叫 main_test.go,因為測試的 Fibonacci 函數在 main.go 文件里。
- 單元測試的函數名必須以 Test 開頭,是可導出的、公開的函數。
- 測試函數的簽名必須接收一個指向 testing.T 類型的指針,并且不能返回任何值。
- 函數名最好是 Test + 要測試的函數名,比如例子中是 TestFibonacci,表示測試的是 Fibonacci 這個函數。
遵循以上規則,你就可以很容易地編寫單元測試了。單元測試的重點在于熟悉業務代碼的邏輯、場景等,以便盡可能地全面測試,保障代碼質量。
單元測試覆蓋率
以上示例中的 Fibonacci 函數是否被全面地測試了呢?這就需要用單元測試覆蓋率進行檢測了。
Go 語言提供了非常方便的命令來查看單元測試覆蓋率。還是以 Fibonacci 函數的單元測試為例,通過一行命令即可查看它的單元測試覆蓋率。
? go test -v --coverprofile=ch18.cover ./ch18
這行命令包括 --coverprofile 這個 Flag,它可以得到一個單元測試覆蓋率文件,運行這行命令還可以同時看到測試覆蓋率。Fibonacci 函數的測試覆蓋率如下:
PASS coverage: 85.7% of statements ok gotour/ch18 0.367s coverage: 85.7% of statements
可以看到,測試覆蓋率為 85.7%。從這個數字來看,Fibonacci 函數應該沒有被全面地測試,這時候就需要查看詳細的單元測試覆蓋率報告了。
運行如下命令,可以得到一個 HTML 格式的單元測試覆蓋率報告:
? go tool cover -html=ch18.cover -o=ch18.html
命令運行后,會在當前目錄下生成一個 ch18.html 文件,使用瀏覽器打開它,可以看到圖中的內容:
單元測試覆蓋率報告
紅色標記的部分是沒有測試到的,綠色標記的部分是已經測試到的。這就是單元測試覆蓋率報告的好處,通過它你可以很容易地檢測自己寫的單元測試是否完全覆蓋。
根據報告,我再修改一下單元測試,把沒有覆蓋的代碼邏輯覆蓋到,代碼如下:
fsMap[-1] = 0
也就是說,由于圖中 n<0 的部分顯示為紅色,表示沒有測試到,所以我們需要再添加一組測試用例,用于測試 n<0 的情況?,F在再運行這個單元測試,查看它的單元測試覆蓋率,就會發現已經是 100% 了。
基準測試
除了需要保證我們編寫的代碼的邏輯正確外,有時候還有性能要求。那么如何衡量代碼的性能呢?這就需要基準測試了。
什么是基準測試
基準測試(Benchmark)是一項用于測量和評估軟件性能指標的方法,主要用于評估你寫的代碼的性能。
Go 語言的基準測試
Go 語言的基準測試和單元測試規則基本一樣,只是測試函數的命名規則不一樣。現在還以 Fibonacci 函數為例,演示 Go 語言基準測試的使用。
Fibonacci 函數的基準測試代碼如下:
ch18/main_test.go
func BenchmarkFibonacci(b *testing.B){ for i:=0;i<b.N;i++{ Fibonacci(10) } }
這是一個非常簡單的 Go 語言基準測試示例,它和單元測試的不同點如下:
- 基準測試函數必須以 Benchmark 開頭,必須是可導出的;
- 函數的簽名必須接收一個指向 testing.B 類型的指針,并且不能返回任何值;
- 最后的 for 循環很重要,被測試的代碼要放到循環里;
- b.N 是基準測試框架提供的,表示循環的次數,因為需要反復調用測試的代碼,才可以評估性能。
寫好了基準測試,就可以通過如下命令來測試 Fibonacci 函數的性能:
? go test -bench=. ./ch18
goos: darwin
goarch: amd64
pkg: gotour/ch18
BenchmarkFibonacci-8 3461616 343 ns/op
PASS
ok gotour/ch18 2.230s
運行基準測試也要使用 go test 命令,不過要加上 -bench 這個 Flag,它接受一個表達式作為參數,以匹配基準測試的函數,"."表示運行所有基準測試。
下面著重解釋輸出的結果??吹胶瘮岛竺娴?-8 了嗎?這個表示運行基準測試時對應的 GOMAXPROCS 的值。接著的 3461616 表示運行 for 循環的次數,也就是調用被測試代碼的次數,最后的 343 ns/op 表示每次需要花費 343 納秒。
基準測試的時間默認是 1 秒,也就是 1 秒調用 3461616 次、每次調用花費 343 納秒。如果想讓測試運行的時間更長,可以通過 -benchtime 指定,比如 3 秒,代碼如下所示:
go test -bench=. -benchtime=3s ./ch18
計時方法
進行基準測試之前會做一些準備,比如構建測試數據等,這些準備也需要消耗時間,所以需要把這部分時間排除在外。這就需要通過 ResetTimer 方法重置計時器,示例代碼如下:
func BenchmarkFibonacci(b *testing.B) { n := 10 b.ResetTimer() //重置計時器 for i := 0; i < b.N; i++ { Fibonacci(n) } }
這樣可以避免因為準備數據耗時造成的干擾。
除了 ResetTimer 方法外,還有 StartTimer 和 StopTimer 方法,幫你靈活地控制什么時候開始計時、什么時候停止計時。
內存統計
在基準測試時,還可以統計每次操作分配內存的次數,以及每次操作分配的字節數,這兩個指標可以作為優化代碼的參考。要開啟內存統計也比較簡單,代碼如下,即通過 ReportAllocs() 方法:
func BenchmarkFibonacci(b *testing.B) { n := 10 b.ReportAllocs() //開啟內存統計 b.ResetTimer() //重置計時器 for i := 0; i < b.N; i++ { Fibonacci(n) } }
現在再運行這個基準測試,就可以看到如下結果:
? go test -bench=. ?./ch18
goos: darwin
goarch: amd64
pkg: gotour/ch18
BenchmarkFibonacci-8 ?2486265 ?486 ns/op ?0 B/op ?0 allocs/op
PASS
ok ? ? ?gotour/ch18 ? ? 2.533s
可以看到相比原來的基準測試多了兩個指標,分別是 0 B/op 和 0 allocs/op。前者表示每次操作分配了多少字節的內存,后者表示每次操作分配內存的次數。這兩個指標可以作為代碼優化的參考,盡可能地越小越好。
小提示:以上兩個指標是否越小越好?這是不一定的,因為有時候代碼實現需要空間換時間,所以要根據自己的具體業務而定,做到在滿足業務的情況下越小越好。
并發基準測試
除了普通的基準測試外,Go 語言還支持并發基準測試,你可以測試在多個 goroutine 并發下代碼的性能。還是以 Fibonacci 為例,它的并發基準測試代碼如下:
func BenchmarkFibonacciRunParallel(b *testing.B) { n := 10 b.RunParallel(func(pb *testing.PB) { for pb.Next() { Fibonacci(n) } }) }
可以看到,Go 語言通過 RunParallel 方法運行并發基準測試。RunParallel 方法會創建多個 goroutine,并將 b.N 分配給這些 goroutine 執行。
基準測試實戰
相信你已經理解了 Go 語言的基準測試,也學會了如何使用,現在我以一個實戰幫你復習。
還是以 Fibonacci 函數為例,通過前面小節的基準測試,會發現它并沒有分配新的內存,也就是說 Fibonacci 函數慢并不是因為內存,排除掉這個原因,就可以歸結為所寫的算法問題了。
在遞歸運算中,一定會有重復計算,這是影響遞歸的主要因素。解決重復計算可以使用緩存,把已經計算好的結果保存起來,就可以重復使用了。
基于這個思路,我將 Fibonacci 函數的代碼進行如下修改:
//緩存已經計算的結果 var cache = map[int]int{} func Fibonacci(n int) int { if v, ok := cache[n]; ok { return v } result := 0 switch { case n < 0: result = 0 case n == 0: result = 0 case n == 1: result = 1 default: result = Fibonacci(n-1) + Fibonacci(n-2) } cache[n] = result return result }
這組代碼的核心在于采用一個 map 將已經計算好的結果緩存、便于重新使用。改造后,我再來運行基準測試,看看剛剛優化的效果,如下所示:
BenchmarkFibonacci-8 97823403 11.7 ns/op
可以看到,結果為 11.7 納秒,相比優化前的 343 納秒,性能足足提高了 28 倍。
總結
單元測試是保證代碼質量的好方法,但單元測試也不是萬能的,使用它可以降低 Bug 率,但也不要完全依賴。除了單元測試外,還可以輔以 Code Review、人工測試等手段更好地保證代碼質量。
在這節課的最后給你留個練習題:在運行 go test 命令時,使用 -benchmem 這個 Flag 進行內存統計。
原文鏈接:https://learn.lianglianglee.com
相關推薦
- 2024-04-04 mybatis-config.xml的配置
- 2022-03-19 解析SQL?Server?CDC配合Kafka?Connect監聽數據變化的問題_MsSql
- 2022-04-04 scrapy框架中用ssh連接遠程服務器的實現_python
- 2022-06-30 MongoDB排序時內存大小限制與創建索引的注意事項詳解_MongoDB
- 2022-11-20 Go項目實現優雅關機與平滑重啟功能_Golang
- 2022-06-25 C#實現連接電子秤串口自動稱重_C#教程
- 2023-02-10 數據卷(Data?Volumes)及dockefile詳解_docker
- 2022-11-25 Linux?apache實現https的配置方法_Linux
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支