關于 Golang 的模糊測試實踐
導言
在 Go 編程領域,有一個提升代碼安全性的秘密武器:模糊測試(fuzz testing)。想象一下,有個機器人不知疲倦的向你的 Go 程序扔出除了廚房水槽以外的所有東西,以確保它們堅如磐石。模糊測試不是常規(guī)、可預測的測試,而是測試意料之外的、離奇的場景,用隨機數據挑戰(zhàn)代碼,以發(fā)現隱藏 bug。
Go 的出現讓模糊測試變得輕而易舉。由于工具鏈內置了支持,Go 開發(fā)人員可以輕松的將這種強大的測試方法自動化。這就像為代碼配備了時刻保持警惕的守護者,不斷查找那些可能會漏掉的偷偷摸摸的 bug。
Go 模糊測試就是要將代碼推向極限,甚至超越極限,以確保代碼在現實世界中能夠抵御任何奇特而美妙的輸入。這證明了 Go 對可靠性和安全性的承諾,在一個軟件需要堅如磐石的世界里,它能讓人高枕無憂。
因此,如果你發(fā)現應用程序即使在最意想不到的情況下也能流暢運行時,請記住模糊測試所發(fā)揮的作用,它作為無名英雄在幕后為 Go 應用的順利運行而努力。
種子語料庫(Seed Corpus):高效模糊測試的基礎
種子語料庫是提供給模糊測試流程的初始輸入集合,用于啟動生成測試用例,可以把它想象成鎖匠用來制作萬能鑰匙的初始鑰匙集。在模糊測試中,這些種子作為起點,模糊器從中衍生出多種變體,探索大量可能的輸入以發(fā)現錯誤。通過精心挑選一組具有代表性的多樣化種子,可以確保模糊器從一開始就能覆蓋更多領域,從而使測試過程更高效且有效。種子可以是典型用例數據,也可以是邊緣用例或以前發(fā)現的可誘發(fā)錯誤的輸入,從而為徹底測試軟件的可靠性奠定基礎。
示例:對 Go 字符串反轉函數進行模糊測試
我們用 Go 編寫一個簡單的字符串反轉函數,然后創(chuàng)建一個模糊測試。這個示例將有助于說明模糊測試如何在看似簡單的函數中發(fā)現意想不到的行為或錯誤。
Go 函數:反轉字符串
package main
// ReverseString takes a string as input and returns its reverse.
func ReverseString(s string) string {
// Convert the string to a rune slice to properly handle multi-byte characters.
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
// Swap the runes.
runes[i], runes[j] = runes[j], runes[i]
}
// Convert the rune slice back to a string and return it.
return string(runes)
}
解釋:
- ReverseString 函數:該函數接收一個字符串參數并返回其反轉值。它將字符串作為 rune 切片而不是字節(jié)來處理,這對于正確處理大小可能超過一個字節(jié)的 Unicode 字符至關重要。
- Rune 切片:通過將字符串轉換為 rune 切片,可以確保正確處理多字節(jié)字符,保持字符編碼的完整性。
- 交換:該函數迭代 rune 切片,從兩端開始交換元素,然后向中心移動,從而有效反轉切片。
ReverseString 函數的模糊測試
我們?yōu)檫@個函數編寫模糊測試:
package main
import (
"testing"
"unicode/utf8"
)
// FuzzReverseString tests the ReverseString function with fuzzing.
func FuzzReverseString(f *testing.F) {
// Seed corpus with examples, including a case with Unicode characters.
f.Add("hello")
f.Add("world")
f.Add("こんにちは") // "Hello" in Japanese
f.Fuzz(func(t *testing.T, original string) {
// Reverse the string twice should give us the original string back.
reversed := ReverseString(original)
doubleReversed := ReverseString(reversed)
if original != doubleReversed {
t.Errorf("Double reversing '%s' did not give original string, got '%s'", original, doubleReversed)
}
// The length of the original and the reversed string should be the same.
if utf8.RuneCountInString(original) != utf8.RuneCountInString(reversed) {
t.Errorf("The length of the original and reversed string does not match for '%s'", original)
}
})
}
解釋:
- 種子語料庫:我們從一組種子輸入開始,包括簡單的 ASCII 字符串和一個 Unicode 字符串,以確保模糊測試涵蓋一系列字符編碼。
- 模糊函數:模糊函數反轉字符串,然后再反轉回來,期望得到原始字符串。這是一個簡單的不變量,如果反轉函數正確的話,就應該總是成立的。它還會檢查原始字符串和反轉字符串的長度是否相同,以檢查多字節(jié)字符可能出現的問題。
- 運行測試:要運行該模糊測試,請使用帶有 -fuzz 標志的 go test 命令,如:go test -fuzz=Fuzz。
構建用于數據持久化的 Go REST API 的模糊測試
要在 Go 中創(chuàng)建一個接受 POST 請求并將接收到的數據存儲到文件中的 REST API,可以使用 net/http 軟件包。我們將為處理 POST 請求數據的函數編寫模糊測試。請注意,由于模糊測試的性質及其適用性,此處的模糊測試將重點測試數據處理邏輯,而非 HTTP 服務器本身。
步驟 1:處理 POST 請求的 REST API 函數
首先需要設置一個簡單的 HTTP 服務器,保證其路由可以處理 POST 請求。該服務器將把 POST 請求正文保存到文件中。
package main
import (
"io/ioutil"
"log"
"net/http"
)
func main() {
http.HandleFunc("/save", saveDataHandler) // Set up the route
log.Println("Server starting on port 8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// saveDataHandler saves the POST request body into a file.
func saveDataHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
return
}
// Read the body of the POST request
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
// Save the data into a file
err = ioutil.WriteFile("data.txt", body, 0644)
if err != nil {
http.Error(w, "Error saving file", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Data saved successfully"))
}
這個簡單的服務監(jiān)聽 8080 端口,并有一個接受 POST 請求的路由 /save。該路由的處理程序 saveDataHandler 會讀取請求正文并將其寫入名為 data.txt 的文件中。
步驟 2:編寫模糊測試
在模糊測試中,我們將重點關注將數據保存到文件中的功能。由于無法直接對 HTTP 服務器進行模糊測試,我們把處理數據的邏輯提取到單獨的函數中,并對其進行模糊測試。
package main
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
)
// FuzzSaveDataHandler uses f.Fuzz to fuzz the body of POST requests sent to saveDataHandler.
func FuzzSaveDataHandler(f *testing.F) {
// Seed corpus with examples, including different types and lengths of data.
f.Add([]byte("example data")) // Example seed
f.Add([]byte("")) // Empty seed
f.Fuzz(func(t *testing.T, data []byte) {
// Construct a new HTTP POST request with fuzzed data as the body.
req, err := http.NewRequest(http.MethodPost, "/save", bytes.NewReader(data))
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
// Create a ResponseRecorder to act as the target of the HTTP request.
rr := httptest.NewRecorder()
// Invoke the saveDataHandler with our request and recorder.
saveDataHandler(rr, req)
// Here, you can add assertions based on the expected behavior of your handler.
// For example, checking that the response status code is http.StatusOK.
if rr.Code != http.StatusOK {
t.Errorf("Expected status OK for input %v, got %v", data, rr.Code)
}
// Additional assertions can be added here, such as verifying the response body
// or the content of the "data.txt" file if necessary.
})
}
解釋:
- FuzzSaveDataHandler 函數:該函數測試 saveDataHandler 如何處理不同 POST 請求體,基于模糊測試來嘗試各種輸入數據。
- 種子語料庫:測試從一些示例數據("example data"和空字符串)開始,以指導模糊處理過程。
執(zhí)行模糊測試:
- 對于每個模糊輸入,都會向處理程序發(fā)出 POST 請求。
- ResponseRecorder會捕捉處理程序對這些請求的響應。
- 測試將檢查處理程序是否對所有輸入都響應 http.StatusOK 狀態(tài),從而判斷是否已成功處理這些輸入。
運行測試使用 go test -fuzz=FuzzSaveDataHandler 運行模糊測試。測試從種子數據中生成各種輸入,并檢查處理程序響應。
在 Go 中驗證和存儲 CSV 數據:模糊測試方法
要創(chuàng)建一個讀取 CSV 文件、驗證其值并將驗證后的數據存儲到文件中的函數,我們將按照以下步驟進行操作:
- 處理 CSV 的函數:該函數將讀取 CSV 數據,根據預定義規(guī)則驗證其內容(為簡單起見,假設我們期望兩列具有特定的數據類型),然后將驗證后的數據存儲到新文件中。
- 模糊測試:我們將為驗證 CSV 數據的函數部分編寫模糊測試。這是因為模糊測試非常適合測試代碼如何處理各種輸入,而我們將重點關注驗證邏輯。
步驟 1:處理和驗證 CSV 數據的功能
package main
import (
"encoding/csv"
"fmt"
"io"
"os"
"strconv"
)
// validateAndSaveData reads CSV data from an io.Reader, validates it, and saves valid rows to a file.
func validateAndSaveData(r io.Reader, outputFile string) error {
csvReader := csv.NewReader(r)
validData := [][]string{}
for {
record, err := csvReader.Read()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("error reading CSV data: %w", err)
}
if validateRecord(record) {
validData = append(validData, record)
}
}
return saveValidData(validData, outputFile)
}
// validateRecord checks if a CSV record is valid. For simplicity, let's assume the first column should be an integer and the second a non-empty string.
func validateRecord(record []string) bool {
if len(record) != 2 {
return false
}
if _, err := strconv.Atoi(record[0]); err != nil {
return false
}
if record[1] == "" {
return false
}
return true
}
// saveValidData writes the validated data to a file.
func saveValidData(data [][]string, outputFile string) error {
file, err := os.Create(outputFile)
if err != nil {
return fmt.Errorf("error creating output file: %w", err)
}
defer file.Close()
csvWriter := csv.NewWriter(file)
for _, record := range data {
if err := csvWriter.Write(record); err != nil {
return fmt.Errorf("error writing record to file: %w", err)
}
}
csvWriter.Flush()
return csvWriter.Error()
}
步驟 2:驗證邏輯的模糊測試
在模糊測試中,我們將重點關注 validateRecord 函數,該函數負責驗證 CSV 數據的各個行。
package main
import (
"strings"
"testing"
)
// FuzzValidateRecord tests the validateRecord function with fuzzing.
func FuzzValidateRecord(f *testing.F) {
// Seed corpus with examples, joined as single strings
f.Add("123,validString") // valid record
f.Add("invalidInt,string") // invalid integer
f.Add("123,") // invalid string
f.Fuzz(func(t *testing.T, recordStr string) {
// Split the string back into a slice
record := strings.Split(recordStr, ",")
// Now you can call validateRecord with the slice
_ = validateRecord(record)
// Here you can add checks to verify the behavior of validateRecord
})
}
運行模糊測試:
要運行這個模糊測試,需要使用帶有 -fuzz 標志的 go test 命令:
go test -fuzz=Fuzz
該命令將啟動模糊處理過程,根據提供的種子自動生成和測試各種輸入。
說明:
- validateAndSaveData 函數從io.Reader讀取數據,從而可以處理來自任何實現此接口的數據源(如文件或內存緩沖區(qū))的數據。該函數通過 csv.Reader 解析 CSV 數據,使用 validateRecord 驗證每條記錄,并存儲有效記錄。
- validateRecord 函數旨在根據簡單的規(guī)則驗證每條 CSV 記錄:第一列必須可轉換為整數,第二列必須是非空字符串。
- saveValidData 函數獲取經過驗證的數據,并以 CSV 格式將其寫入指定的輸出文件。
- validateRecord 的模糊測試使用種子輸入來啟動模糊處理過程,用大量生成的輸入值來測試驗證邏輯,以發(fā)現潛在的邊緣情況或意外行為。
測試似乎一直在進行
fuzz: elapsed: 45s, execs: 7257 (0/sec), new interesting: 0 (total: 2)
fuzz: elapsed: 48s, execs: 7257 (0/sec), new interesting: 0 (total: 2)
fuzz: elapsed: 51s, execs: 7257 (0/sec), new interesting: 0 (total: 2)
fuzz: elapsed: 54s, execs: 7257 (0/sec), new interesting: 0 (total: 2)
fuzz: elapsed: 57s, execs: 7257 (0/sec), new interesting: 0 (total: 2)
fuzz: elapsed: 1m0s, execs: 7257 (0/sec), new interesting: 0 (total: 2)
fuzz: elapsed: 1m3s, execs: 7848 (197/sec), new interesting: 4 (total: 6)
fuzz: elapsed: 1m6s, execs: 9301 (484/sec), new interesting: 4 (total: 6)
fuzz: elapsed: 1m9s, execs: 11457 (718/sec), new interesting: 4 (total: 6)
fuzz: elapsed: 1m12s, execs: 14485 (1009/sec), new interesting: 4 (total: 6)
fuzz: elapsed: 1m15s, execs: 16927 (814/sec), new interesting: 4 (total: 6)
當模糊測試似乎無限期或長時間運行時,通常意味著它在不斷生成和測試新的輸入。模糊測試是一個密集的過程,會消耗大量時間和資源,尤其是當被測功能涉及復雜操作或模糊器發(fā)現許多"有趣"的輸入,從而探索出新的代碼路徑時。
以下是可以采取的幾個步驟,用于管理和減少長時間運行的模糊測試:
1.限制模糊測試時間
可以在運行模糊測試時使用 -fuzztime 標志來限制模糊測試的持續(xù)時間。例如,要使模糊測試最多運行 1 分鐘,可以使用:
go test -fuzz=FuzzSaveDataHandler -fuzztime=1m
2.審查和優(yōu)化測試代碼
如果代碼的某些部分特別慢或消耗資源,請考慮盡可能對其進行優(yōu)化。由于模糊測試會產生大量請求,即使代碼效率稍微低一點,也會被放大。
3.調整種子語料庫
檢查提供給模糊器的種子語料庫,確保其多樣性足以探索各種代碼路徑,但又不會過于寬泛,導致模糊器陷入過多路徑。有時,過于通用的種子會導致模糊器在無益路徑上花費過多時間。
4.監(jiān)控"有趣的"輸入
模糊器會報告覆蓋新代碼路徑或觸發(fā)獨特行為的"有趣"輸入。如果"有趣"輸入的數量大幅增加,則可能表明模糊器正在不斷發(fā)現新的探索場景。查看這些輸入可以深入了解代碼中的潛在邊緣情況或意外行為。
5.分析模糊器性能
輸出顯示了每秒執(zhí)行次數,可以讓我們了解模糊器的運行效率。如果執(zhí)行率很低,可能說明模糊器設置或被測代碼存在性能瓶頸。調查并解決這些瓶頸有助于提高模糊器的效率。
6.考慮手動中斷
如果模糊測試運行時間過長而沒有提供額外價值(例如,沒有發(fā)現新的有趣案例,或者已經從當前運行中獲得了足夠信息),可以手動停止該進程,然后查看迄今為止獲得的結果,以決定下一步行動(例如調整模糊參數或調查已發(fā)現的案例)。