模糊测试可以简单快速的自动化构建测试用例,尽量遍历各种可能的输入场景,从而保证函数代码覆盖尽可能多的边缘场景。Go 原生内置了模糊测试的支持,如果善加利用,可以有效提升 Go 代码的质量。原文: Fuzz Testing in Golang
题图由 Lexica 生成
导言
在 Go 编程领域,有一个提升代码安全性的秘密武器:模糊测试(fuzz testing)。想象一下,有个机器人不知疲倦的向你的 Go 程序扔出除了厨房水槽以外的所有东西,以确保它们坚如磐石。模糊测试不是常规、可预测的测试,而是测试意料之外的、离奇的场景,用随机数据挑战代码,以发现隐藏 bug。
Go 的出现让模糊测试变得轻而易举。由于工具链内置了支持,Go 开发人员可以轻松的将这种强大的测试方法自动化。这就像为代码配备了时刻保持警惕的守护者,不断查找那些可能会漏掉的偷偷摸摸的 bug。
Go 模糊测试就是要将代码推向极限,甚至超越极限,以确保代码在现实世界中能够抵御任何奇特而美妙的输入。这证明了 Go 对可靠性和安全性的承诺,在一个软件需要坚如磐石的世界里,它能让人高枕无忧。
因此,如果你发现应用程序即使在最意想不到的情况下也能流畅运行时,请记住模糊测试所发挥的作用,它作为无名英雄在幕后为 Go 应用的顺利运行而努力。
种子语料库(Seed Corpus):高效模糊测试的基础
种子语料库是提供给模糊测试流程的初始输入集合,用于启动生成测试用例,可以把它想象成锁匠用来制作万能钥匙的初始钥匙集。在模糊测试中,这些种子作为起点,模糊器从中衍生出多种变体,探索大量可能的输入以发现错误。通过精心挑选一组具有代表性的多样化种子,可以确保模糊器从一开始就能覆盖更多领域,从而使测试过程更高效且有效。种子可以是典型用例数据,也可以是边缘用例或以前发现的可诱发错误的输入,从而为彻底测试软件的可靠性奠定基础。
示例:对 Go 字符串反转函数进行模糊测试
我们用 Go 编写一个简单的字符串反转函数,然后创建一个模糊测试。这个示例将有助于说明模糊测试如何在看似简单的函数中发现意想不到的行为或错误。
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 切片而不是字节来处理,这对于正确处理大小可能超过一个字节的 Unicode 字符至关重要。
Rune 切片:通过将字符串转换为 rune 切片,可以确保正确处理多字节字符,保持字符编码的完整性。
交换:该函数迭代 rune 切片,从两端开始交换元素,然后向中心移动,从而有效反转切片。
ReverseString 函数的模糊测试
我们为这个函数编写模糊测试:
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 字符串,以确保模糊测试涵盖一系列字符编码。
模糊函数:模糊函数反转字符串,然后再反转回来,期望得到原始字符串。这是一个简单的不变量,如果反转函数正确的话,就应该总是成立的。它还会检查原始字符串和反转字符串的长度是否相同,以检查多字节字符可能出现的问题。
运行测试:要运行该模糊测试,请使用带有 -fuzz
标志的 go test
命令,如:go test -fuzz=Fuzz
。
构建用于数据持久化的 Go REST API 的模糊测试
要在 Go 中创建一个接受 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"))
}
复制代码
这个简单的服务监听 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.
})
}
复制代码
解释:
执行模糊测试
对于每个模糊输入,都会向处理程序发出 POST 请求。
ResponseRecorder
会捕捉处理程序对这些请求的响应。
测试将检查处理程序是否对所有输入都响应 http.StatusOK
状态,从而判断是否已成功处理这些输入。
运行测试使用 go test -fuzz=FuzzSaveDataHandler
运行模糊测试。测试从种子数据中生成各种输入,并检查处理程序响应。
在 Go 中验证和存储 CSV 数据:模糊测试方法
要创建一个读取 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
命令:
该命令将启动模糊处理过程,根据提供的种子自动生成和测试各种输入。
说明
validateAndSaveData
函数从io.Reader
读取数据,从而可以处理来自任何实现此接口的数据源(如文件或内存缓冲区)的数据。该函数通过 csv.Reader
解析 CSV 数据,使用 validateRecord
验证每条记录,并存储有效记录。
validateRecord
函数旨在根据简单的规则验证每条 CSV 记录:第一列必须可转换为整数,第二列必须是非空字符串。
saveValidData
函数获取经过验证的数据,并以 CSV 格式将其写入指定的输出文件。
validateRecord
的模糊测试使用种子输入来启动模糊处理过程,用大量生成的输入值来测试验证逻辑,以发现潜在的边缘情况或意外行为。
测试似乎一直在进行
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)
复制代码
当模糊测试似乎无限期或长时间运行时,通常意味着它在不断生成和测试新的输入。模糊测试是一个密集的过程,会消耗大量时间和资源,尤其是当被测功能涉及复杂操作或模糊器发现许多"有趣"的输入,从而探索出新的代码路径时。
以下是可以采取的几个步骤,用于管理和减少长时间运行的模糊测试:
1.限制模糊测试时间
可以在运行模糊测试时使用 -fuzztime
标志来限制模糊测试的持续时间。例如,要使模糊测试最多运行 1 分钟,可以使用:
go test -fuzz=FuzzSaveDataHandler -fuzztime=1m
复制代码
2.审查和优化测试代码
如果代码的某些部分特别慢或消耗资源,请考虑尽可能对其进行优化。由于模糊测试会产生大量请求,即使代码效率稍微低一点,也会被放大。
3.调整种子语料库
检查提供给模糊器的种子语料库,确保其多样性足以探索各种代码路径,但又不会过于宽泛,导致模糊器陷入过多路径。有时,过于通用的种子会导致模糊器在无益路径上花费过多时间。
4.监控"有趣的"输入
模糊器会报告覆盖新代码路径或触发独特行为的"有趣"输入。如果"有趣"输入的数量大幅增加,则可能表明模糊器正在不断发现新的探索场景。查看这些输入可以深入了解代码中的潜在边缘情况或意外行为。
5.分析模糊器性能
输出显示了每秒执行次数,可以让我们了解模糊器的运行效率。如果执行率很低,可能说明模糊器设置或被测代码存在性能瓶颈。调查并解决这些瓶颈有助于提高模糊器的效率。
6.考虑手动中断
如果模糊测试运行时间过长而没有提供额外价值(例如,没有发现新的有趣案例,或者已经从当前运行中获得了足够信息),可以手动停止该进程,然后查看迄今为止获得的结果,以决定下一步行动(例如调整模糊参数或调查已发现的案例)。
你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!
评论