現在專案的開發都不是一個人可以完成的,需要多人進行協作,那麼在多人協作中如何保證程式碼的質量,你寫的程式碼如何被其他人使用,如何優化程式碼的效能等。
單元測試
在開發完一個功能後,可能會直接把程式碼合併到程式碼庫,用於上線或供其他人使用。但這樣是不對的,因為還沒有對所寫的程式碼進行測試。沒有經過測試的程式碼邏輯可能會存在問題:如果強行合併到程式碼庫,可能影響其他人的開發;如果強行上線,可能導致線上 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 語言函式來計算斐波那契數列就比較簡單了,程式碼如下:
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 檔案用於存放單元測試程式碼。*測試程式碼如下:
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 .
這行命令會執行 目錄下的所有單元測試,因為只寫了一個單元測試,所以可以看到結果如下所示:
➜ go test -v .
=== 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 tools/lib/base (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=base.cover ./base
這行命令包括 –coverprofile 這個 Flag,它可以得到一個單元測試覆蓋率檔案,執行這行命令還可以同時看到測試覆蓋率。Fibonacci 函式的測試覆蓋率如下:
PASS
coverage: 85.7% of statements
ok tools/lib/base 0.367s coverage: 85.7% of statements
可以看到,測試覆蓋率為 85.7%。從這個數字來看,Fibonacci 函式應該沒有被全面地測試,這時候就需要檢視詳細的單元測試覆蓋率報告了。
執行如下命令,可以得到一個 HTML 格式的單元測試覆蓋率報告:
go tool cover -html=base.cover -o=base.html
命令執行後,會在當前目錄下生成一個 base.html 檔案,使用瀏覽器開啟它,可以看到圖中的內容:
紅色標記的部分是沒有測試到的,綠色標記的部分是已經測試到的。這就是單元測試覆蓋率報告的好處,通過它你可以很容易地檢測自己寫的單元測試是否完全覆蓋。
根據報告,再修改一下單元測試,把沒有覆蓋的程式碼邏輯覆蓋到,程式碼如下:
fsMap[-1] = 0
也就是說,由於圖中 n<0 的部分顯示為紅色,表示沒有測試到,所以我們需要再新增一組測試用例,用於測試 n<0 的情況。現在再執行這個單元測試,檢視它的單元測試覆蓋率,就會發現已經是 100% 了。
基準測試
除了需要保證編寫的程式碼的邏輯正確外,有時候還有效能要求。那麼如何衡量程式碼的效能呢?這就需要基準測試了。
什麼是基準測試
基準測試(Benchmark)是一項用於測量和評估軟體效能指標的方法,主要用於評估寫的程式碼的效能。
Go 語言的基準測試
Go 語言的基準測試和單元測試規則基本一樣,只是測試函式的命名規則不一樣。現在還以 Fibonacci 函式為例,演示 Go 語言基準測試的使用。
Fibonacci 函式的基準測試程式碼如下:
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=. ./base
goos: darwin
goarch: amd64
pkg: tools/lib/base
BenchmarkFibonacci-12 4186942 281 ns/op
PASS
ok tools/lib/base 1.927s
執行基準測試也要使用 go test 命令,不過要加上 -bench 這個 Flag,它接受一個表示式作為引數,以匹配基準測試的函式,”.”表示執行所有基準測試。
下面著重解釋輸出的結果。看到函式後面的 -12 了嗎?這個表示執行基準測試時對應的 GOMAXPROCS 的值。接著的 4186942 表示執行 for 迴圈的次數,也就是呼叫被測試程式碼的次數,最後的 281 ns/op 表示每次需要花費 281 納秒。
基準測試的時間預設是 1 秒,也就是 1 秒呼叫 4186942 次、每次呼叫花費 281 納秒。如果想讓測試執行的時間更長,可以通過 -benchtime 指定,比如 3 秒,程式碼如下所示:
go test -bench=. -benchtime=3s ./base
計時方法
進行基準測試之前會做一些準備,比如構建測試資料等,這些準備也需要消耗時間,所以需要把這部分時間排除在外。這就需要通過 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=. ./base
goos: darwin
goarch: amd64
pkg: tools/lib/base
BenchmarkFibonacci-12 4083074 288 ns/op 0 B/op 0 allocs/op
PASS
ok tools/lib/base 2.059s
可以看到相比原來的基準測試多了兩個指標,分別是 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-12 128674249 9.29 ns/op
可以看到,結果為 9.29 納秒,相比優化前的 281 納秒,效能足足提高了 30 倍。
總結
單元測試是保證程式碼質量的好方法,但單元測試也不是萬能的,使用它可以降低 Bug 率,但也不要完全依賴。除了單元測試外,還可以輔以 Code Review、人工測試等手段更好地保證程式碼質量。
本作品採用《CC 協議》,轉載必須註明作者和本文連結