Go 1.18 系列篇(四):一文掌握 Fuzzing 模糊測試
系列導(dǎo)讀
體驗 Go 1.18(一):如何快速升級安裝 Go 1.18 ?
體驗 Go 1.18(三):一文掌握 Go 工作區(qū)模式
# 1. 什么是模糊測試?
單元測試,大家應(yīng)該都寫過吧?單元測試,需要開發(fā)者根據(jù)函數(shù)邏輯,給定幾組輸入(入?yún)ⅲ┡c輸出(返回)的數(shù)據(jù),然后 go test 根據(jù)這些數(shù)據(jù)集,調(diào)用函數(shù),若返回值與預(yù)期相符,則說明函數(shù)的單元測試通過。
但單元測試的代碼,也是由開發(fā)者寫的一段一段代碼,只要是代碼,就會有 BUG,就會有遺漏的場景。
因此即使單元測試通過,也不代表你的程序沒有問題。
可見,測試場景的數(shù)據(jù)集對于測試有多重要,而 Fuzzing 模糊測試就是一種用機器根據(jù)已知數(shù)據(jù)源,來自動生成測試數(shù)據(jù)的一種方案。
本文借用官方的一個例子來講解。
# 2. 簡單的示例
在開始之前,先初始化項目
go?mod?init?github.com/iswbm/fuzz
然后在該項目中添加 ?main.go,內(nèi)容如下
package?main
import?"fmt"
func?Reverse(s?string)?string?{
????b?:=?[]?byte(s)
????for?i,?j?:=?0,?len(b)-1;?i?len(b)/2;?i,?j?=?i+1,?j-1?{
????????b[i],?b[j]?=?b[j],?b[i]
????}
????return?string(b)
}
func?main()?{
????input?:=?"The?quick?brown?fox?jumped?over?the?lazy?dog"
????rev?:=?Reverse(input)
????doubleRev?:=?Reverse(rev)
????fmt.Printf("original:?%q\n",?input)
????fmt.Printf("reversed:?%q\n",?rev)
????fmt.Printf("reversed?again:?%q\n",?doubleRev)
}
現(xiàn)在我們要為 Reverse 函數(shù)編寫單元測試代碼,放在 reverse_test.go,Test 函數(shù)如下
給定了三組數(shù)據(jù)
遍歷這幾組數(shù)據(jù),將 tc.in 做為 Reverses 函數(shù)的入?yún)?zhí)行函數(shù),其返回值跟預(yù)期的 tc.want 做對比
若不相等,則測試不通過~
package?main
import?(
????"testing"
)
func?TestReverse(t?*testing.T)?{
????testcases?:=?[]struct?{
????????in,?want?string
????}{
????????{"Hello,?world",?"dlrow?,olleH"},
????????{"?",?"?"},
????????{"!12345",?"54321!"},
????}
????for?_,?tc?:=?range?testcases?{
????????rev?:=?Reverse(tc.in)
????????if?rev?!=?tc.want?{
????????????????t.Errorf("Reverse:?%q,?want?%q",?rev,?tc.want)
????????}
????}
}
現(xiàn)在我們執(zhí)行 go test 即是普通的單元測試,輸出 PASS 說明單元測試通過,到目前為止是 Go 1.18 之前的單元測試

然后我們再往 reverse_test.go 中加入 Fuzzing 模糊測試的代碼
//?記得前面導(dǎo)入?"unicode/utf8"?包
func?FuzzReverse(f?*testing.F)?{
????testcases?:=?[]string{"Hello,?world",?"?",?"!12345"}
????for?_,?tc?:=?range?testcases?{
????????f.Add(tc)??//?Use?f.Add?to?provide?a?seed?corpus
????}
????f.Fuzz(func(t?*testing.T,?orig?string)?{
????????rev?:=?Reverse(orig)
????????doubleRev?:=?Reverse(rev)
????????if?orig?!=?doubleRev?{
????????????t.Errorf("Before:?%q,?after:?%q",?orig,?doubleRev)
????????}
????????if?utf8.ValidString(orig)?&&?!utf8.ValidString(rev)?{
????????????t.Errorf("Reverse?produced?invalid?UTF-8?string?%q",?rev)
????????}
????})
}
Fuzzing 模糊測試的代碼格式與單元測試很像:
函數(shù)名固定以 Fuzz 開頭(單元測試是以 Test 開頭)
函數(shù)固定以 *testing.F 類型做為入?yún)ⅲ▎卧獪y試是以 *testing.T)
不一樣的是 Fuzzing 模糊測試,提供兩個函數(shù):
t.Add:用于開發(fā)者輸入模糊測試的種子數(shù)據(jù),fuzzing 根據(jù)這些種子數(shù)據(jù),自動隨機生成更多測試數(shù)據(jù)
t.Fuzz:開始運行模糊測試,t.Fuzz 的入?yún)⑹且粋€ Fuzz Target 函數(shù)(官方這么叫的),這個 Fuzz Target 函數(shù)的編寫邏輯跟單元測試就一樣了
在本例子中,F(xiàn)uzz Target 接收 類型為 string 的入?yún)?,做?Reverse 的輸入源,然后利用兩次 Reverse 的結(jié)果應(yīng)與原字符串相等的原理進行測試。
有了 FuzzReverse 函數(shù)后,就可以使用如下命令進行模糊測試
go18?test?-fuzz=Fuzz
通過輸出發(fā)現(xiàn)測試并不順利,Go 1.18 的 Fuzzing 會將導(dǎo)致測試異常的數(shù)據(jù)文件記錄下來,使用 cat 可以查看該測試數(shù)據(jù)

記錄下來后,該數(shù)據(jù)就可做為普通單元測試的數(shù)據(jù),此時我們再執(zhí)行 go test 就會引用該數(shù)據(jù),當(dāng)然了,在問題解決之前, go test 會一直報錯

# 3. 問題排查與解決
模糊測試幫我們發(fā)現(xiàn)了一個出乎意料的 Bug 場景:在中文里的字符 泃其實是由3個字節(jié)組成的,如果按照字節(jié)反轉(zhuǎn),反轉(zhuǎn)后得到的就是一個無效的字符串。
因此為了保證字符串反轉(zhuǎn)后得到的仍然是一個有效的UTF-8編碼的字符串,我們要按照rune進行字符串反轉(zhuǎn)。
為了更好地方便大家理解中文里的字符 泃按照rune為維度有多少個rune,以及按照byte反轉(zhuǎn)后得到的結(jié)果長什么樣,我們對代碼做一些修改。

改完之后,再次執(zhí)行 go test 就會提示測試成功,說明我們已經(jīng)修復(fù)上面的那個場景的 BUG

當(dāng)下我們已經(jīng)發(fā)現(xiàn)并修復(fù)了一個 BUG,程序肯定還有更多 BUG 存在,要繼續(xù)尋找可以再次進行模糊測試,重復(fù)上面的步驟即可,這里不再贅述。
# 4. 更多參數(shù)介紹
在支持了 Fuzzing 模糊測試后,go test 工具也有了一些新的命令,在這里一并記錄下
進行模糊測試
go?test?-fuzz=Fuzz
只對某個函數(shù)進行模糊測試:使用 -run=Fuzzxxx 或者 -fuzz=Fuzzxxx 指定模糊測試函數(shù),避免執(zhí)行到其他測試函數(shù)
go18?test?-run=FuzzReverse
go18?test?-fuzz=FuzzReverse
測試某個失敗數(shù)據(jù):使用 -run=file 指定數(shù)據(jù)文件
go?test?-run=FuzzReverse/1fdd0160e6b3dd8f1e6b7a4179b4787e0c014cf9c46c67a863d71e3a0277c213
指定模糊測試的時間:使用 -fuzztime 指定模糊測試時間或者迭代次數(shù)(默認(rèn)無限期),避免一直在跑測試無法退出
還有一個 -fuzzminimizetime 參數(shù),看官方文檔的介紹,我沒明白其作用,有知道的還請評論區(qū)分享下
go?test?-fuzz=Fuzz?-fuzztime?30s
設(shè)置模糊測試進程數(shù)據(jù):默認(rèn)值是 $GOMAXPROCS,可根據(jù)實際情況進行設(shè)置,避免太占用機器的資源
go?test?-fuzz=Fuzz?-parallel?4
# 5. 寫在最后
模糊測試的存在,并不是為了替代原單元測試,而是為單元測試提供更好的保障,是一個補充方案,而非替代方案。
單元測試的局限性在于,你只能用預(yù)期的輸入進行測試;模糊測試在發(fā)現(xiàn)暴露出奇怪行為的意外輸入方面非常出色。一個好的模糊測試系統(tǒng)也會對被測試的代碼進行分析,因此它可以有效地產(chǎn)生輸入,從而擴大代碼覆蓋面。
同時模糊測試的適用場景也比較有限,如果函數(shù)的入?yún)⒉⒉皇窍癖纠械哪菢拥暮唵危ㄗ址?,而是各種對象呢?可能它就無能為力了吧。
模糊測試的功能,對你有幫助嗎?歡迎你留言分享~
? ?

喜歡明哥文章的同學(xué)歡迎點擊卡片訂閱!
???
