使用go語言開發自動化API測試工具

程序设计实验室發表於2024-03-08

前言

上一篇文章說到我還開發了一個獨立的自動測試工具,可以根據 OpenAPI 的文件來測試,並且在測試完成後輸出測試報告,報告內容包括每個介面是否測試透過和響應時間等。

這個工具我使用了 go 語言開發,主要是考慮到了 go 語言可以傻瓜式的實現交叉編譯,生成的可執行檔案直接上傳到伺服器就可以執行,非常方便。

PS: go 語言寫起來是真的折磨!感覺語法有很多彆扭的地方,不過 build 的時候實在太爽了,根本無法拒絕😂

為了避免篇幅太長,本文先介紹用到的元件,詳細實現以及解析 OpenAPI 文件生成測試配置的部分後續的文章再介紹。

網路請求

標準庫中的 net/http 包提供了傳送 HTTP 請求的功能,拿到資料之後,使用 json.Unmarshal 函式解析 JSON 資料。這個包相對比較低階,對於簡單的網路請求,夠用,不過我還是想選擇更好用的元件。

Resty 是一個簡單而強大的 Go HTTP 客戶端,具有鏈式 API,可以輕鬆地傳送 HTTP 請求並處理 JSON 資料。它提供了豐富的功能,包括自動重試、超時設定、請求和響應日誌等。您可以使用 Resty 來傳送 GET、POST、PUT、DELETE 等各種型別的請求,並且它能夠自動將響應的 JSON 資料解析為 Go 結構體。

現在出了 v2 版本,支援 HTTP/2、WebSocket、Cookie 操作,並提供了更加簡潔和易用的 API 。

專案地址: https://github.com/go-resty/resty

使用起來還行

GET 方法

import 	"github.com/go-resty/resty/v2"

req := c.RestyClient.R().SetHeader("Authorization", "token "+c.AuthToken)

req.SetQueryParams(map[string]string{
  "year":  "2024",
})
resp, err = req.Get("path")

POST 方法

req.SetBody(map[string]string{
  "year":  "2024",
})
resp, err = req.Get("path")

SetBody 的引數是 interface{} 型別,可以傳入的型別比較豐富,我這裡還是跟 GET 一樣傳了字典,實際上應該傳 struct 比較多一些吧。

日誌元件

我之前用的是 go 語言內建的 log ,但似乎功能很少,也沒有日誌等級啥的,這能叫日誌庫嗎……

接著我找到了在 GitHub 上 star 很多的 logrus 庫,不過感覺這是一個比較古老的庫了,不太好用,formatter 也沒找到好用的,看專案主頁的介紹發現這個庫已經進入退休狀態…

它讓我 Check out, for example, Zerolog, Zap, and Apex.

Logrus is in maintenance-mode. We will not be introducing new features. It's simply too hard to do in a way that won't break many people's projects, which is the last thing you want from your Logging library (again...).

專案地址: https://github.com/sirupsen/logrus

所以,最終還是用了 uber 的日誌庫 go.uber.org/zap

logrus 使用 & 配置

雖然後面換了 zap ,還是記錄一下關於 logrus 的使用。

專案主頁上列舉的幾個第三方 formatter 我基本都試用了,就這個 nested-logrus-formatter 比較好用。

以下配置實現了同時輸出日誌到控制檯和檔案。

import (
  nested "github.com/antonfisher/nested-logrus-formatter"
  "github.com/sirupsen/logrus"
  "os"
)

func initLogger() *os.File {
  logger.SetLevel(logrus.DebugLevel)
  logger.SetReportCaller(true)
  logger.SetFormatter(&nested.Formatter{})

  // 建立一個檔案作為日誌輸出
  file, err := os.OpenFile("logfile.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
  if err != nil {
    logger.Fatalf("無法開啟日誌檔案: %v", err)
  }

  // 建立一個多寫入器,將日誌同時輸出到控制檯和檔案
  mw := io.MultiWriter(os.Stdout, file)

  // 新增 Hook 到 Logger 中
  logger.Out = mw

  return file
}

func main() {
  file := initLogger()
  defer func(file *os.File) {
    err := file.Close()
    if err != nil {
      fmt.Println(err)
    }
  }(file)
}

zap 使用 & 配置

zap 比起 logrus 好用多了,開箱即用,搭配 zapcore 可以配置多個輸出,也可以設定按日誌大小分割檔案,還可以對接其他日誌收集平臺啥的,基本做到了現代日誌元件的水平了…

一樣是實現了同時輸出日誌到控制檯和檔案。

import (
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"os"
)

func buildLogger() *zap.SugaredLogger {
  config := zap.NewProductionEncoderConfig()
  config.EncodeTime = zapcore.ISO8601TimeEncoder
  consoleEncoder := zapcore.NewConsoleEncoder(config)
  fileEncoder := zapcore.NewJSONEncoder(config)
  logFile, _ := os.OpenFile("./log-test-zap.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 06666)

  tee := zapcore.NewTee(
    zapcore.NewCore(fileEncoder, zapcore.AddSync(logFile), zap.DebugLevel),
    zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), zap.DebugLevel),
  )
  var zapLogging = zap.New(
    tee,
    zap.AddCaller(),
    zap.AddStacktrace(zapcore.ErrorLevel),
  )

  var logger = zapLogging.Sugar()
  return logger
}

func main() {
  logger := buildLogger()
  defer logger.Sync()
}

swagger 元件

這裡我試著用了一下 go swagger

專案地址: https://github.com/go-swagger/go-swagger

這個可以使用 scoop 安裝

scoop install go-swagger

用著一般,沒有 swagger code generator 好用。

就沒繼續探索下去了。

PS: Jetbrains 系的 IDE 裡有幾成 swagger code generator 工具,功能非常強大,可以生成各種程式碼。

不過我還是自己實現了OpenAPI 文件解析(比較靈活),所以暫時還沒用上這個強大的工具。

Excel 匯出

Excel 操作我用的是這個 github.com/xuri/excelize/v2

一開始沒注意,後面發現這個居然是奇安信開源的…… 然後看了 qax-os 這個 group ,發現開源的幾個專案都是跟安全無關的,不務正業啊老哥!😂

沒有對比其他的,看著 star 挺多,上手就直接用了

感覺還行。

func exportTestReportsToExcel(testReports []*tester.Report, filename string) error {
  // 建立一個新的 Excel 檔案
  f := excelize.NewFile()

  // 建立一個名為 "測試報告" 的工作表
  index, err := f.NewSheet("測試報告")
  if err != nil {
    return err
  }

  // 設定工作表列名
  f.SetCellValue("測試報告", "A1", "介面名稱")
  f.SetCellValue("測試報告", "B1", "介面路徑")
  f.SetCellValue("測試報告", "C1", "測試是否透過")
  f.SetCellValue("測試報告", "D1", "耗時(秒)")

  // 遍歷測試報告並在工作表中寫入資料
  for i, report := range testReports {
    row := i + 2
    f.SetCellValue("測試報告", fmt.Sprintf("A%d", row), report.ApiName)
    f.SetCellValue("測試報告", fmt.Sprintf("B%d", row), report.ApiPath)
    f.SetCellValue("測試報告", fmt.Sprintf("C%d", row), func() string {
      if report.IsPassed {
        return "是"
      }
      return "否"
    }())
    f.SetCellValue("測試報告", fmt.Sprintf("D%d", row), report.Elapsed.Seconds())
  }

  // 設定活動工作表
  f.SetActiveSheet(index)

  // 將 Excel 檔案儲存到磁碟
  err = f.SaveAs(filename)
  if err != nil {
    return err
  }

  return nil
}

吐槽

三元表示式

我很想吐槽 go 為啥沒有三元表示式,用匿名函式真的好繁瑣啊!!

據說是因為覺得三元表示式可以寫出很多讓人看不懂的騷程式碼,所以 go 不打算支援,因噎廢食啊😂

不過這難不倒我,可以寫個函式來模擬,而且現在 go 似乎更新了泛型的功能,不用再拿 interface 來模擬

func If[T any](condition bool, trueVal, falseVal T) T {
  if condition {
    return trueVal
  }
  return falseVal
}

使用的時候就

result := If[string](report.IsPassed, "成功", "沒透過")

支援型別推導,所以 [string] 也可以省略了。

這樣前面匯出 Excel 的程式碼裡的匿名函式就可以改成這樣,簡潔多了!

f.SetCellValue("測試報告", fmt.Sprintf("C%d", row), If(report.IsPassed, "是", "否"))

陣列排序

本來也不算什麼吐槽,屬於是挑刺了,go 的排序沒那麼好用,但也不難用。

用匿名函式可以實現按欄位排序,這倒是和 C 語言裡用函式指標大同小異,不愧是帶 gc 的 C 語言

測試報告的資料結構是這樣

// Report 測試報告
type Report struct {
  ApiName  string
  ApiPath  string
  IsPassed bool
  Elapsed  time.Duration
  Response *ApiResponse
}

我想對 []*Report 陣列排序,可以用 sort.Slice 方法

// 按照 Elapsed 屬性排序,從大到小
sort.Slice(testReports, func(i, j int) bool {
  return testReports[i].Elapsed > testReports[j].Elapsed
})

相比之下還是 cs 的 Linq 舒服啊

testReports.Sort((a, b) => a - b);

小結

就這樣吧,很簡單的一個小工具,因為還處在 go 的小白階段,每用一個新的庫都會記錄一下。

參考資料

  • uber的Go日誌庫zap使用詳解 -https://www.cnblogs.com/jiujuan/p/17304844.html

相關文章