使用 go test 测试 gin 的开发接口 | 青训营

30 阅读3分钟

前面的话

本篇针对 gin 中的 GETPOST 路由方法。

  • GET: 通过 URL 获取参数
  • POST: 通过 PostForm 获取参数

路由配置

下面对于 GETPOST 路由定义如下

func GetRouter() *gin.Engine {
   r := gin.Default()
   // base interfaces
   r.GET("/testget", func(c *gin.Context) {
      var a, b int
      var err error

      if a, err = strconv.Atoi(c.Query("a")); err != nil {
         c.JSON(http.StatusBadRequest, err.Error())
         return
      }
      if b, err = strconv.Atoi(c.Query("b")); err != nil {
         c.JSON(http.StatusBadRequest, err.Error())
         return
      }

      c.JSON(http.StatusOK, map[string]int{"res": a + b})
   })

   r.POST("/testpost", func(c *gin.Context) {
      var a, b int
      var err error

      if a, err = strconv.Atoi(c.PostForm("a")); err != nil {
         c.JSON(http.StatusBadRequest, err.Error())
         return
      }
      if b, err = strconv.Atoi(c.PostForm("b")); err != nil {
         c.JSON(http.StatusBadRequest, err.Error())
         return
      }

      f, err := c.FormFile("filename") // 上传时文件的参数名
      if err = c.SaveUploadedFile(f, "filepath"); err != nil {
         // 文件保存路径
         c.JSON(http.StatusBadRequest, err.Error())
      }

      c.JSON(http.StatusOK, gin.H{"res": a + b})
   })
   return r
}

GET 方法测试

func TestGetSuccess(t *testing.T) {
   r := gintest.GetRouter()
   data := url.Values{}
   data.Add("a", strconv.Itoa(1))
   data.Add("b", strconv.Itoa(2))
   req, err := http.NewRequest(http.MethodGet, "/testget", nil)
   if err != nil {
      t.Fatalf("构建请求失败: %s", err.Error())
   }
   req.URL.RawQuery = data.Encode()
   record := httptest.NewRecorder()
   r.ServeHTTP(record, req)
   res := record.Result()
   if res.StatusCode != http.StatusOK {
      t.Fatalf("状态码不符合预期: %d", res.StatusCode)
   }
   buf := make([]byte, 1024)
   n, err := res.Body.Read(buf)
   if err != nil {
      t.Fatalf("读取错误: %s", err.Error())
   }
   body := map[string]int{}
   if err = json.Unmarshal(buf[:n], &body); err != nil {
      t.Fatalf("解析失败: %s", err.Error())
   }
   if body["res"] != 3 {
      t.Fatalf("返回结果不符合预期: %d", body["res"])
   }
}

func TestGetFail(t *testing.T) {
   r := gintest.GetRouter()
   data := url.Values{}
   data.Add("a", strconv.Itoa(1))
   data.Add("b", "test input error")
   req, err := http.NewRequest(http.MethodGet, "/testget", nil)
   if err != nil {
      t.Fatalf("Build request failed, err: %v", err)
   }
   req.URL.RawQuery = data.Encode()
   record := httptest.NewRecorder()
   r.ServeHTTP(record, req)
   res := record.Result()
   if res.StatusCode != http.StatusBadRequest {
      t.Fatalf("状态码不符合预期: %d", res.StatusCode)
   }
   buf := make([]byte, 1024)
   n, err := res.Body.Read(buf)
   if err != nil {
      t.Fatalf("读取错误: %s", err.Error())
   }
   var errMsg string
   if err = json.Unmarshal(buf[:n], &errMsg); err != nil {
      t.Fatalf("解析失败: %s", err.Error())
   }
   if errMsg != "strconv.Atoi: parsing "test input error": invalid syntax" {
      t.Fatalf("返回结果不符合预期: %s", errMsg)
   }
}
  • r 是需要被测试的路由
  • data 构建请求参数
  • reqhttp 请求体
  • res 为返回 HTTP 通过 r.ServeHTTP 方法获得请求结果
  • buf 需要足够大的空间来容纳返回结果,以供接下来的解析
  • body/errMsg 是返回体,可以自定义结构体来进行解析。
  • 通过校验以上所有参数即可完成简单的使用 go test 测试 gin
  • 对于更复杂的错误判断逻辑应该实现更多的 Test 函数

POST 方法测试

func TestPostSuccess(t *testing.T) {
   testUploadFile := "./go.mod"
   r := gintest.GetRouter()
   payload := &bytes.Buffer{}
   writer := multipart.NewWriter(payload)
   _ = writer.WriteField("a", "1")
   _ = writer.WriteField("b", "2")

   var (
      file *os.File
      part io.Writer
      err  error
   )
   if file, err = os.Open(testUploadFile); err != nil {
      t.Fatalf("打开文件失败: %s", err.Error())
   }
   defer file.Close()

   if part, err = writer.CreateFormFile("filename", filepath.Base(testUploadFile)); err != nil {
      t.Fatalf("创建文件参数失败: %s", err.Error())
   }

   if _, err = io.Copy(part, file); err != nil {
      t.Fatalf("文件参数数据拷贝失败: %s", err.Error())
   }
   if err = writer.Close(); err != nil {
      t.Fatalf("参数写入 PostForm 失败: %s", err.Error())
   }
   req, err := http.NewRequest(http.MethodPost, "/testpost", payload)
   if err != nil {
      t.Fatalf("构建请求体失败: %s", err.Error())
   }
   req.Header.Set("Content-Type", writer.FormDataContentType())
   record := httptest.NewRecorder()
   r.ServeHTTP(record, req)
   res := record.Result()
   if res.StatusCode != http.StatusOK {
      t.Fatalf("状态码不符合预期: %d", res.StatusCode)
   }
   buf := make([]byte, 1024)
   n, err := res.Body.Read(buf)
   if err != nil {
      t.Fatalf("读取错误: %s", err.Error())
   }
   body := map[string]int{}
   if err = json.Unmarshal(buf[:n], &body); err != nil {
      t.Fatalf("解析失败: %s", err.Error())
   }
   if body["res"] != 3 {
      t.Fatalf("返回结果不符合预期: %d", body["res"])
   }
}

func TestPostFail(t *testing.T) {
   testUploadFile := "./go.mod"
   r := gintest.GetRouter()
   payload := &bytes.Buffer{}
   writer := multipart.NewWriter(payload)
   _ = writer.WriteField("a", "1")
   _ = writer.WriteField("b", "test input error")

   var (
      file *os.File
      part io.Writer
      err  error
   )
   if file, err = os.Open(testUploadFile); err != nil {
      t.Fatalf("打开文件失败: %s", err.Error())
   }
   defer file.Close()

   if part, err = writer.CreateFormFile("filename", filepath.Base(testUploadFile)); err != nil {
      t.Fatalf("创建文件参数失败: %s", err.Error())
   }

   if _, err = io.Copy(part, file); err != nil {
      t.Fatalf("文件参数数据拷贝失败: %s", err.Error())
   }
   if err = writer.Close(); err != nil {
      t.Fatalf("参数写入 PostForm 失败: %s", err.Error())
   }
   req, err := http.NewRequest(http.MethodPost, "/testpost", payload)
   if err != nil {
      t.Fatalf("构建请求体失败: %s", err.Error())
   }
   req.Header.Set("Content-Type", writer.FormDataContentType())
   record := httptest.NewRecorder()
   r.ServeHTTP(record, req)
   res := record.Result()
   if res.StatusCode != http.StatusBadRequest {
      t.Fatalf("状态码不符合预期: %d", res.StatusCode)
   }
   buf := make([]byte, 1024)
   n, err := res.Body.Read(buf)
   if err != nil {
      t.Fatalf("读取错误: %s", err.Error())
   }
   var errMsg string
   if err = json.Unmarshal(buf[:n], &errMsg); err != nil {
      t.Fatalf("解析失败: %s", err.Error())
   }
   if errMsg != "strconv.Atoi: parsing "test input error": invalid syntax" {
      t.Fatalf("返回结果不符合预期: %s", errMsg)
   }
}
  • GET 方法基本一致
  • 不同点在于参数的构建与写入方法
  • POST 方法还要在请求头中写入请求体的格式
  • POST 可以上传文件这样的其他格式参数
  • POST 方法的测试小技巧: 因为 POST 方法有各种输入格式,这里利用 Postman 可以快速得到请求现有服务器的方法,只要将服务器换成本地的 gin 路由器就可以在 go test 中进行测试。

image.png

image.png 注意把 clientres 的获取方式换掉即可。