讓寫測試的代碼量驟減,你會開始寫測試嗎?
模糊測試是一種向程序提供隨機意外的輸入以測試可能的崩潰或者邊緣情況的方法。通過模糊測試可以揭示一些邏輯錯誤或者性能問題,因此使用模糊測試可以讓程序的穩定性和性能都更有保證。
Go 從1.18 版本開始正式把模糊測試(Go Fuzz)加入到了其工具集中,不再依靠三方庫就能在程序代碼中進行模糊測試。那么為什么要引入模糊測試呢,引入后我們在寫單元測試的時候要有哪些調整呢?
首先我們來聊聊為什么引入模糊測試。
為什么引入模糊測試
大家看文章開頭第一段的解釋,那就是Go官方要引入模糊測試的原因。估計各位看了想要打人,哈,那我就結合個簡單的例子再把上面那段話要表達的意思,用代碼再解釋一遍。
大家先不考慮什么模糊測試的事兒,就單純給下面這個工具函數寫一個單測,我們該怎么寫。
func Equal(a []byte, b []byte) bool {
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
這個工具函數將接收兩個字節切片,比較他們的值是否相等。那么為了通過單測測試這個工具函數是否能如預期那樣完成任務,我們就需要提供一些樣本數據,來測試函數的知識結果。
單元測試怎么寫
我們在之前Go 單元測試入門中,給大家介紹過表格測試,就是為單測的執行提供樣本數據的,那么這個表格測試該怎么寫呢?這里直接放代碼了,如果對表格測試和各種Go單測知識不了解的可以回看之前的文章:Go單元測試基礎,文末會給出鏈接。
func TestEqualWithTable(t *testing.T) {
tests := []struct {
name string
inputA []byte
inputB []byte
want bool
}{
{"right case", []byte{'f', 'u', 'z', 'z'}, []byte{'f', 'u', 'z', 'z'}, true},
{"right case", []byte{'a', 'b', 'c'}, []byte{'b', 'c', 'd'}, false},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
if got := Equal(tt.inputA, tt.inputB); got != tt.want {
t.Error("expected " + strconv.FormatBool(tt.want) + ", got " +
strconv.FormatBool(got))
}
})
}
}
上面這個單元測試使用的兩個樣本數據能讓測試通過,但不代表我們的工具函數就完美無缺了,畢竟這里的兩個樣本都太典型了,如果你把輸入的兩個切片搞的不一樣,工具函數直接就index out of range,程序直接掛掉了。
如果沒有模糊測試呢,我們就需要在表格測試里盡量多的提供樣本,才能測出各種邊界情況下程序是否符合預期。
不過讓自己提供樣本測試,主觀性太強,有的人能想到很多邊界條件有的就不行,再加上我國互聯網公司程序員糟糕的職場生存環境,又要保證BUG少穩定,又要快,這個時候模糊測試確實能幫助我們節省很多想樣本的工作量。
用模糊測試簡化
現在我們換用Go 1.18 的 Fuzz 模糊測試,來測試下我們的工具函數。
func FuzzEqual(f *testing.F) {
//f.Add([]byte{'a', 'b', 'c'}, []byte{'a', 'b', 'c'})
f.Fuzz(func(t *testing.T, a []byte, b []byte) {
Equal(a, b)
})
}
雖然模糊測試是1.18 新引入的,但只是節省了我們寫表格測試提供樣本的流程,其他流程和以前的單元測試并不差別,所用到的知識也沒有變化。
可以看到使用模糊測試后,代碼量明顯減少了很多。模糊測試會幫我們生產隨機的輸入,來供要測試的目標來使用。上面兩個參數的輸入是隨機產生的(也有規則,模糊測試會先測各種空輸入,這個規則我們可以不用管)
也可以通過f.Add()方法添加語料,注意這里語料設置的個數和順序要和目標函數里的輸入參數保持一致(就是除了 testing.T之外的參數)
此外還有點明顯的差異大家一定要注意,使用模糊測試后,測試函數的聲明跟普通單測的不一樣
// 普通單元測試
TestXXX(t *testing.T){}
// 使用模糊測試的測試函數,必須以Fuzz開頭,形參類型為*testing.F
FuzzXXX(f *testing.F) {}
執行模糊測試
模糊測試執行的時候需要給 go test加上-fuzz這個標記。
? go test -fuzz . warning: starting with empty corpus fuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0 (total: 0) fuzz: minimizing 57-byte failing input file fuzz: elapsed: 0s, minimizing --- FAIL: FuzzEqual (0.04s) --- FAIL: FuzzEqual (0.00s) testing.go:1349: panic: runtime error: index out of range [0] with length 0
執行模糊測試后,就能測出我上面說的索引越界的問題,這個時候我們就可以回去完善我們的工具函數,然后再進行模糊測試了,通過幾輪執行,會讓被測試的函數足夠健壯。
我們示例的工具函數足夠簡單,所以修復起來也超簡單,價格長度判斷就可以了。
func Equal(a []byte, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
再度執行模糊測試后程序不再會報錯,不過這個時候你應該發現,測試程序會一直執行,除非主動停下來,或者發現了測試失敗的情況才能讓模糊測試終止。
這就是模糊測試和普通單測的另一個大區別了,普通單測執行完我們提供的 Case 后就會停止,而模糊測試是會不停的跑樣本,直到發生測試失敗的情況才會停止。這個時候我們就可以用命令指定一個測試時長,讓模糊測試到時自動停止。
go test -fuzz=Fuzz -fuzztime=10s .
這里我們通過 fuzztime 這個標志,給模糊測試指定了 10 s的測試時長,到時模糊測試就會自動停止。
? go test -fuzz=Fuzz -fuzztime=10s . fuzz: elapsed: 0s, gathering baseline coverage: 0/10 completed fuzz: elapsed: 0s, gathering baseline coverage: 10/10 completed, now fuzzing with 8 workers fuzz: elapsed: 3s, execs: 852282 (284056/sec), new interesting: 0 (total: 10) fuzz: elapsed: 6s, execs: 1748599 (298745/sec), new interesting: 0 (total: 10) fuzz: elapsed: 9s, execs: 2653073 (301474/sec), new interesting: 0 (total: 10) fuzz: elapsed: 10s, execs: 2955038 (274558/sec), new interesting: 0 (total: 10) PASS ok golang-unit-test-demo/fuzz_test_demo 11.783s ? fuzz_test_demo git:(master) ?
怎么寫好一個模糊測試
相信通過上面的例子,其實大家已經看到模糊測試該怎么編寫。為了讓內容更吸引人,文章并沒有上來就給大家羅列一堆名稱概念,這里我們再把理論上的一些東西補充一下,這樣未來自己編寫模糊測試的時候自己心里就更有譜啦。
模糊測試的結構
下面是官方文檔里,給出的一張 "模糊測試構成元素" 的圖

模糊測試的結構,來自:https://go.dev/doc/fuzz/
這張圖里能看出來這幾點:
- Fuzz test: 即整個模糊測試函數,它的函數簽名要求,函數名必須以關鍵字 Fuzz 開頭,只有一個類型為
*testing.F的參數,且沒有返回值。 - 模糊測試也是測試,所以跟單測一樣,必須位于
_test.go文件中,可以語單測在統一文件。 - Fuzz Target:模糊測試中,由 f.Fuzz 指定的要執行的測試函數叫 fuzz target,一個模糊測試中只能包含一個 fuzz target,且它的第一個參數必須是
*testing.T類型的,后面跟至少一個模糊參數,這個也好理解,如果沒有這個參數,那隨機輸入該往哪輸入呢。 - Fuzz argument:這個一條說過了,就是fuzz target 中第一個參數以后的參數都叫模糊參數,用來接收模糊測試隨機生成的樣本,這個參數數量應該是要跟我們的被測函數的形參數一致的。
- Seed corpus:語料,這個單詞兒我也沒見過,大家記住就是提供了它后,生產的隨機參數都跟這個語料有相關性,不是瞎隨機的,且用 f.Add 設置的語料個數,要跟模糊參數的個數、順序、類型上保持一致。
更詳細的解釋,請參考官方文檔:https://go.dev/doc/fuzz/
總結
模糊測試對于檢測我們看不到或想不到的錯誤、邊界情況很有用,即使我們的常規測試具有出色的覆蓋率,話說,諸位,你們的測試覆蓋率真的很出色嗎,還是說完全沒有測試,嘿。
還有一點要注意的是,模糊測試期間比較耗內存,所以假如想在CI 流水線里加入模糊測試的運行時,需要考量一下資源耗費的問題,定一個適合的運行時長。