关于 Golang 的模糊测试实践

导言

  在 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.
   })
  }

解释:

  ·FuzzSaveDataHandler 函数:该函数测试 saveDataHandler 如何处理不同 POST 请求体,基于模糊测试来尝试各种输入数据。

  · 种子语料库:测试从一些示例数据("example data"和空字符串)开始,以指导模糊处理过程。

  执行模糊测试:

  · 对于每个模糊输入,都会向处理程序发出 POST 请求。

  · ResponseRecorder会捕捉处理程序对这些请求的响应。

  · 测试将检查处理程序是否对所有输入都响应 http.StatusOK 状态,从而判断是否已成功处理这些输入。

  运行测试使用 go test -fuzz=FuzzSaveDataHandler 运行模糊测试。测试从种子数据中生成各种输入,并检查处理程序响应。

  在 Go 中验证和存储 CSV 数据:模糊测试方法

  要创建一个读取 CSV 文件、验证其值并将验证后的数据存储到文件中的函数,我们将按照以下步骤进行操作:

  · 处理 CSV 的函数:该函数将读取 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 命令:

 

感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值