Golang之模糊測(cè)試工具的使用
背景
我們經(jīng)常調(diào)侃程序員每天都在寫(xiě)bug,這確實(shí)是事實(shí),沒(méi)有測(cè)出bug不代表程序就真的不存在問(wèn)題。傳統(tǒng)的代碼review、靜態(tài)分析、人工測(cè)試和自動(dòng)化的單元測(cè)試無(wú)法窮盡所有輸入組合,尤其是難以模擬一些隨機(jī)的、邊緣的數(shù)據(jù)。
去年6月,Go官方發(fā)布稱gotip版本已經(jīng)原生支持Fuzzing并開(kāi)始了公測(cè),將與[Go 1.18版本]一起在2022年中發(fā)布,go-fuzzing至今已經(jīng)發(fā)現(xiàn)了Go標(biāo)準(zhǔn)庫(kù)超過(guò)200個(gè)bug(https://github.com/dvyukov/go-fuzz#trophies )。即將發(fā)布的[Go 1.18版本]就提供了一個(gè)代碼自測(cè)的絕佳工具go-fuzzing。
說(shuō)到[Go 1.18版本],大家最關(guān)注的應(yīng)該是泛型,但是我個(gè)人覺(jué)得go-fuzzing也是其中的一個(gè)亮點(diǎn),Go 1.18將fuzz testing納入了go test工具鏈,與單元測(cè)試、性能基準(zhǔn)測(cè)試等一起成為了Go原生測(cè)試工具鏈中的重要成員。
本次就來(lái)說(shuō)下go-fuzzing這個(gè)工具。
開(kāi)發(fā)環(huán)境
升級(jí)到Go 1.18
Go 1.18雖然還沒(méi)正式發(fā)布,但可以下載RC版本,而且即使你生產(chǎn)環(huán)境用是Go的老版本,你個(gè)人的本地開(kāi)發(fā)環(huán)境也可以升級(jí)到1.18,還可以使用go-fuzzing更好的自測(cè)
go-fuzzing
官方文檔:go fuzzing是通過(guò)持續(xù)給一個(gè)程序不同的輸入來(lái)自動(dòng)化測(cè)試,并通過(guò)分析代碼覆蓋率來(lái)智能的尋找失敗的例子。這種方法可以盡可能的找到一些邊界問(wèn)題,親測(cè)確實(shí)發(fā)現(xiàn)的都是些平時(shí)比較難發(fā)現(xiàn)的問(wèn)題。
fuzzing,又叫fuzz testing,中文叫做模糊測(cè)試或隨機(jī)測(cè)試。其本質(zhì)上是一種自動(dòng)化測(cè)試技術(shù),更具體一點(diǎn),它是一種基于隨機(jī)輸入的自動(dòng)化測(cè)試技術(shù),常被用于發(fā)現(xiàn)處理用戶輸入的代碼中存在的bug和問(wèn)題。
fuzz tests規(guī)則
func FuzzFoo(f *testing.F) { ?? ?f.Add(5, "hello") ?? ?f.Fuzz(func(t *testing.T, i int, s string) { ?? ??? ?out, err := Foo(i, s) ?? ??? ?if err != nil && out != "" { ?? ??? ??? ?t.Errorf("%q, %v", out, err) ?? ??? ?} ?? ?}) }
- 函數(shù)必須是Fuzz開(kāi)頭,唯一的參數(shù)只有*testing.F,沒(méi)有返回值
- Fuzz tests必須在名為*_test.go的文件下才能執(zhí)行
- fuzz target是個(gè)方法,它調(diào)用(*testing.F).Fuzz,第一個(gè)參數(shù)是 *testing.T,之后的參數(shù)就是稱之為fuzzing arguments的參數(shù),方法沒(méi)有返回值
- 每個(gè)fuzz test中只能有一個(gè)fuzz target
- 調(diào)用f.Add()的時(shí)候需要參數(shù)類型跟fuzzing arguments順序和類型都保持一致
- fuzzing arguments只支持以下類型:
- int, int8, int16, int32/rune, int64
- uint, uint8/byte, uint16, uint32, uint64
- string, []byte
- float32, float64
- bool
如何使用go-fuzzing
1、首先要先定義fuzzing arguments,并通過(guò)fuzzing arguments寫(xiě)fuzzing target
2、思考fuzzing target怎么寫(xiě),重點(diǎn)是怎么驗(yàn)證結(jié)果的正確性,因?yàn)閒uzzing arguments是隨機(jī)給的,所以要有個(gè)驗(yàn)證結(jié)果的方法
3、遇到失敗的例子怎么去打印出錯(cuò)誤結(jié)果
4、根據(jù)錯(cuò)誤結(jié)果去生成新的測(cè)試用例,這個(gè)新的測(cè)試用例會(huì)被用來(lái)調(diào)試發(fā)現(xiàn)的bug,并且可以留下給CI使用
下面是一個(gè)切片中數(shù)字求和的例子:
// slice_sum.go func SliceSum(arr []int64) int64 { ? var sum int64 ? for _, val := range arr { ? ? if val % 100000 != 0 { ? ? ? sum += val ? ? } ? } ? return sum }
第一步:定義fuzzing arguments模糊參數(shù)
至少需要給出一個(gè)fuzzing arguments,不然go-fuzzing沒(méi)法生成測(cè)試代碼。
這是切片中元素求和的方法,那我們可以把切片的元素個(gè)數(shù)n(自行模擬個(gè)數(shù)即可)作為fuzzing arguments,然后go-fuzzing會(huì)根據(jù)運(yùn)行的代碼覆蓋率自動(dòng)生成不同的參數(shù)來(lái)模擬測(cè)試。
// slice_sum_test.go func FuzzSliceSum(f *testing.F) { ? // 10,go-fuzzing稱之為語(yǔ)料,10這個(gè)值就是讓go fuzzing冷啟動(dòng)的一個(gè)值,具體多少不重要 ? f.Add(10) ? f.Fuzz(func(t *testing.T, n int) { ? ?? ?// 限制20個(gè)元素 ? ? n %= 20 ? ? // 剩余處理 ? }) }
第二步:編寫(xiě)fuzzing target
重點(diǎn)是編寫(xiě)可以驗(yàn)證的fuzzing target,不僅要根據(jù)給定的模糊參數(shù)寫(xiě)出測(cè)試代碼,而且還需要生成可以驗(yàn)證結(jié)果正確性的數(shù)據(jù)。
對(duì)這個(gè)切片元素求和的方法來(lái)說(shuō),就是隨機(jī)生成n個(gè)元素的切片,然后進(jìn)行求和得到正確的結(jié)果。
package fuzz import ( ?? ?"github.com/stretchr/testify/assert" ?? ?"math/rand" ?? ?"testing" ?? ?"time" ) // slice_sum_test.go func FuzzSliceSum(f *testing.F) { ? // 初始化隨機(jī)數(shù)種子 ? rand.Seed(time.Now().UnixNano()) ? // 語(yǔ)料 ? f.Add(10) ? f.Fuzz(func(t *testing.T, n int) { ? ? n %= 20 ? ? var arr []int64 ? ? var expect int64 // 期望值 ? ? for i := 0; i < n; i++ { ? ? ? val := rand.Int63() % 1000000 ? ? ? arr = append(arr, val) ? ? ? expect += val ? ? } ? ? // 自己求和的結(jié)果和調(diào)用函數(shù)求和的結(jié)果比對(duì) ? ? assert.Equal(t, expect, SliceSum(arr)) ? }) }
執(zhí)行模糊測(cè)試
? fuzz go test -fuzz=SliceSum
fuzz: elapsed: 0s, gathering baseline coverage: 0/52 completed
fuzz: elapsed: 0s, gathering baseline coverage: 52/52 completed, now fuzzing with 8 workers
fuzz: elapsed: 0s, execs: 9438 (34179/sec), new interesting: 2 (total: 54)
--- FAIL: FuzzSliceSum (0.28s)
--- FAIL: FuzzSliceSum (0.00s)
slice_sum_test.go:32:
Error Trace: slice_sum_test.go:32
value.go:556
value.go:339
fuzz.go:337
Error: Not equal:
expected: 5715923
actual : 5315923
Test: FuzzSliceSum
Failing input written to testdata/fuzz/FuzzSliceSum/8e8981ffa4ee4d93f475c807563f9d63854a6c913cdfb10a73191549318a2a51
To re-run:
go test -run=FuzzSliceSum/8e8981ffa4ee4d93f475c807563f9d63854a6c913cdfb10a73191549318a2a51
FAIL
exit status 1
FAIL demo/fuzz 0.287s
上面這段輸出,你只能看出預(yù)期值和實(shí)際值不一樣,但是很難分析錯(cuò)誤。
第三步:打印出錯(cuò)誤的例子
上面的錯(cuò)誤輸出,如果能打印出造成錯(cuò)誤的例子的話,就可以直接作為測(cè)試用例進(jìn)行單測(cè)。我們總不能一個(gè)個(gè)去試吧,而且錯(cuò)誤的例子未必只有一個(gè)。
修改下模糊測(cè)試代碼,增加打?。?/p>
package fuzz import ( ?? ?"github.com/stretchr/testify/assert" ?? ?"math/rand" ?? ?"testing" ?? ?"time" ) // slice_sum_test.go func FuzzSliceSum(f *testing.F) { ?? ?// 初始化隨機(jī)數(shù)種子 ?? ?rand.Seed(time.Now().UnixNano()) ?? ?// 語(yǔ)料 ?? ?f.Add(10) ?? ?f.Fuzz(func(t *testing.T, n int) { ?? ??? ?n %= 20 ?? ??? ?var arr []int64 ?? ??? ?var expect int64 // 期望值 ?? ??? ?var buf strings.Builder ?? ??? ?buf.WriteString("\n") ?? ??? ?for i := 0; i < n; i++ { ?? ??? ??? ?val := rand.Int63() % 1000000 ?? ??? ??? ?arr = append(arr, val) ?? ??? ??? ?expect += val ?? ??? ??? ?buf.WriteString(fmt.Sprintf("%d,\n", val)) ?? ??? ?} ?? ??? ?// 自己求和的結(jié)果和調(diào)用函數(shù)求和的結(jié)果比對(duì) ?? ??? ?assert.Equal(t, expect, SliceSum(arr), buf.String()) ?? ?}) }
再次執(zhí)行模糊測(cè)試
? fuzz go test -fuzz=SliceSum
fuzz: elapsed: 0s, gathering baseline coverage: 0/47 completed
fuzz: elapsed: 0s, gathering baseline coverage: 47/47 completed, now fuzzing with 8 workers
fuzz: elapsed: 0s, execs: 17109 (42507/sec), new interesting: 2 (total: 49)
--- FAIL: FuzzSliceSum (0.41s)
--- FAIL: FuzzSliceSum (0.00s)
slice_sum_test.go:34:
Error Trace: slice_sum_test.go:34
value.go:556
value.go:339
fuzz.go:337
Error: Not equal:
expected: 7575516
actual : 7175516
Test: FuzzSliceSum
Messages:
92016,
642504,
400000,
489403,
472011,
811028,
315130,
298207,
57765,
542614,
136594,
351360,
867104,
918715,
515092,
665973,
Failing input written to testdata/fuzz/FuzzSliceSum/9191ba4d7ea5420a9a76661d4e7d6a7a4e69ad4d5d8ef306ff78161a2acf1416
To re-run:
go test -run=FuzzSliceSum/9191ba4d7ea5420a9a76661d4e7d6a7a4e69ad4d5d8ef306ff78161a2acf1416
FAIL
exit status 1
FAIL demo/fuzz 0.413s
第四步:根據(jù)輸出的錯(cuò)誤例子,編寫(xiě)新的測(cè)試用例進(jìn)行單測(cè)
// 單測(cè)通過(guò)后,再執(zhí)行模糊測(cè)試,看看有沒(méi)有其他邊緣問(wèn)題出現(xiàn) func TestSliceSumFuzzCase1(t *testing.T) { arr := []int64{ 92016, 642504, 400000, 489403, 472011, 811028, 315130, 298207, 57765, 542614, 136594, 351360, 867104, 918715, 515092, 665973, } // 期望值從第三步的輸出中獲取 assert.Equal(t, int64(7575516), SliceSum(arr)) }
這樣就可以很方便的進(jìn)行調(diào)試了,并且能夠增加有效的測(cè)試用例進(jìn)行單測(cè),確保這個(gè)bug不會(huì)出現(xiàn)了。
生產(chǎn)環(huán)境項(xiàng)目Go版本問(wèn)題
線上項(xiàng)目的Go版本不能升級(jí)到1.18怎么辦?
線上的版本不升級(jí)到1.18,但是我們本地開(kāi)發(fā)升級(jí)沒(méi)有問(wèn)題,可以在文件的頭部增加如下命令注釋:
slice_sum_test.go
//go:build go1.18 // +build go1.18
這樣我們?cè)诰€上不管用哪個(gè)版本都不會(huì)報(bào)錯(cuò),而且我們一般都是在本地進(jìn)行模糊測(cè)試
注意:第三行必須是空行,不然就會(huì)變成package的注釋了
有些還無(wú)法復(fù)現(xiàn)的問(wèn)題,比如協(xié)程死鎖,輸出一直在執(zhí)行或者卡住然后過(guò)一會(huì)才結(jié)束,這類的長(zhǎng)時(shí)間執(zhí)行的模糊測(cè)試,我還沒(méi)有摸透。如果有大佬知道的話麻煩也告訴我下。
參考
https://github.com/dvyukov/go-fuzz#trophies
https://go.dev/blog/fuzz-beta
到此這篇關(guān)于Golang之模糊測(cè)試工具的使用的文章就介紹到這了,更多相關(guān)Golang 模糊測(cè)試 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章

Go語(yǔ)言開(kāi)發(fā)保證并發(fā)安全實(shí)例詳解

Golang 定時(shí)器(Timer 和 Ticker),這篇文章就夠了

Golang使用JWT進(jìn)行認(rèn)證和加密的示例詳解

使用Golang編寫(xiě)一個(gè)簡(jiǎn)單的命令行工具

GoLang中Json?Tag用法實(shí)例總結(jié)