返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
Go

Build an automated API testing tool using the Go programming language.

I developed this tool using Go language, mainly because Go allows for foolproof cross-compilation, making the generated executable files directly uploadable to the server for immediate execution, which is very convenient.

曦远 (DealiAxy)约 25 分钟
目录12 个章节

Preface

The previous article mentioned that I also developed an independent automated testing tool, which can test based on OpenAPI documentation and output a test report after completion. The report includes details such as whether each API passed the test and the response time.

I developed this tool using the Go language, mainly because Go allows for a foolproof cross-compilation process. The generated executable file can be directly uploaded to the server and run, which is very convenient.

PS: Writing in Go is genuinely frustrating! It feels like the syntax has many awkward parts, but the build process is incredibly satisfying—just impossible to resist 😂

To avoid making the article too long, this piece will first introduce the components used. Detailed implementation and the part about parsing OpenAPI documentation to generate test configurations will be covered in subsequent articles.

Network Requests

The net/http package in the standard library provides the functionality to send HTTP requests. After obtaining the data, the json.Unmarshal function is used to parse the JSON data. This package is relatively low-level and sufficient for simple network requests. However, I still prefer to choose a more user-friendly component.

Resty is a simple yet powerful Go HTTP client with a chainable API that makes it easy to send HTTP requests and handle JSON data. It offers a rich set of features, including automatic retries, timeout settings, request and response logging, and more. You can use Resty to send various types of requests like GET, POST, PUT, DELETE, etc., and it can automatically parse the JSON response data into Go structs.

The v2 version is now available, supporting HTTP/2, WebSocket, Cookie operations, and providing a more concise and user-friendly API.

Project URL: https://github.com/go-resty/resty

Works pretty well.

GET Method

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 Method

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

The parameter for SetBody is of type interface{}, allowing a wide variety of types to be passed. In this example, I passed a map, just like for the GET request. In practice, however, it is more common to pass a struct.

Logging Component

I previously used Go's built-in log, but it seemed to have very few features and lacked log levels—could this even be called a logging library...

Then I found the highly starred logrus library on GitHub, but it felt like a rather outdated library that wasn't very user-friendly. I couldn't find a suitable formatter either, and after checking the project's homepage, I discovered the library has been put into retirement...

It told me to 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...).

Project URL: https://github.com/sirupsen/logrus

So, in the end, I ended up using Uber's logging library go.uber.org/zap.

Logrus Usage & Configuration

Although I switched to zap later, I'm still documenting how to use logrus.

I've tried most of the third-party formatters listed on the project homepage, and only this nested-logrus-formatter works well.

The following configuration implements outputting logs to both the console and a file simultaneously.

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{})

  // Create a file for log output
  file, err := os.OpenFile("logfile.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
  if err != nil {
    logger.Fatalf("Failed to open log file: %v", err)
  }

  // Create a multi-writer to output logs to both console and file
  mw := io.MultiWriter(os.Stdout, file)

  // Add the Hook to the 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 Usage & Configuration

zap is much more user-friendly than logrus and works out of the box. Combined with zapcore, it can be configured for multiple outputs, supports log file splitting by size, and can even integrate with other log collection platforms. It has essentially reached the level of a modern logging component...

As before, it achieves simultaneous logging to both the console and a file.

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 Component

Here I tried using go swagger.

Project repository: https://github.com/go-swagger/go-swagger

It can be installed using scoop.

scoop install go-swagger

My experience was average, not as good as swagger code generator.

So I didn't continue exploring further.

PS: JetBrains IDEs include swagger code generator tools with very powerful features that can generate various code.

However, I implemented OpenAPI document parsing myself (which is more flexible), so I haven't used this powerful tool yet.

Excel Export

For Excel operations, I'm using this library: github.com/xuri/excelize/v2

I didn't pay much attention at first, but later discovered it was actually open-sourced by QiAnXin... Then I looked at the qax-os group and found that the several open-source projects they've released are all unrelated to security. Not sticking to their core business, guys! 😂

I didn't compare it with others. Seeing it had many stars, I just started using it directly.

It feels alright.

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
}

Rant

Ternary Expression (三元表达式)

I really want to complain about why Go doesn't have a ternary expression (三元表达式). Using anonymous functions is truly so cumbersome!!

It's said that the reason is because it's believed that ternary expressions can lead to a lot of clever code that's hard to understand, so Go doesn't plan to support it. That's throwing out the baby with the bathwater. 😂

But this doesn't stop me. I can write a function to simulate it. Also, it seems Go has updated its generics features now, so there's no longer a need to use interface for simulation.

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

When using it, it's simply:

result := If[string](report.IsPassed, "成功", "没通过")

It supports type inference, so the [string] can even be omitted.

Now, the anonymous function in the earlier Excel export code can be changed to this, making it much more concise!

f.SetCellValue("测试报告", fmt.Sprintf("C%d", row), If(report.IsPassed, "是", "否"))

Array Sorting

This isn't really a complaint, more like nitpicking—Go's sorting isn't the most convenient, but it's not difficult either.

You can sort by field using anonymous functions, which is pretty much the same as using function pointers in C—truly a C language with garbage collection

The data structure for the test report is as follows:

// Report test report
type Report struct {
  ApiName  string
  ApiPath  string
  IsPassed bool
  Elapsed  time.Duration
  Response *ApiResponse
}

If I want to sort a []*Report array, I can use the sort.Slice method:

// Sort by the Elapsed property in descending order
sort.Slice(testReports, func(i, j int) bool {
  return testReports[i].Elapsed > testReports[j].Elapsed
})

In comparison, C#'s LINQ is so much more pleasant

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

Summary

That's it. A fairly simple little tool. Since I'm still in the Go beginner stage, I make sure to document each new library I use.

Reference

_EOF_

Build an automated API testing tool using the Go programming language.

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…

Build an automated API testing tool using the Go programming language. · StarBlog