Golang实现单元测试中的接口层

作者:LinkinStar 时间:2024-04-26 17:17:29 

上次我们已经搞定了逻辑层的单元测试,这次我们来康康接口层的单元测试。接口层主要负责的就是请求的处理,最常见的就是 HTTP 请求的处理。

但针对 接口层 的单元测试其实是可以五花八门的。它并不像逻辑层和数据层一样的通用,对于它的测试往往有很多路可以走。

由于使用的 HTTP 框架不同,单元测试的实现方式则不同。 既可以通过程序来模拟 HTTP 请求,也可以通过真实的 HTTP 请求来测试,通过借助外部的一些测试工具来实现。

所以本文只能给出一种思路,具体的实现方式还是要根据实际的框架来实现。

环境

本文以常用的 gin 框架为例,使用一种个人比较喜欢也非常简单的方式来实现单元测试。特点主要有:

  • 不需要启动路由服务

  • 复用已有的项目内的请求结构

代码

由于之前已经贴过,所以 service 层的 代码这里就不赘述了

base case

package controller

import (
   "context"

"github.com/gin-gonic/gin"
   "go-demo/m/unit-test/entity"
)

//go:generate mockgen -source=./user.go -destination=../mock/user_service_mock.go -package=mock
type UserService interface {
   AddUser(ctx context.Context, username string) (err error)
   GetUser(ctx context.Context, userID int) (user *entity.User, err error)
}

type AddUserRequest struct {
   Username string `json:"username" binding:"required"`
}

type GetUserRequest struct {
   UserID int `form:"user_id" binding:"required"`
}

type GetUserResponse struct {
   Username string `json:"username"`
}

type UserController struct {
   UserService UserService
}

func NewUserController(userService UserService) *UserController {
   return &UserController{UserService: userService}
}

func (uc *UserController) AddUser(ctx *gin.Context) {
   req := &AddUserRequest{}
   if err := ctx.BindJSON(req); err != nil {
       return
   }
   if err := uc.UserService.AddUser(ctx, req.Username); err != nil {
       ctx.JSON(400, gin.H{"error": err.Error()})
       return
   }
   ctx.JSON(200, gin.H{"message": "success"})
}

func (uc *UserController) GetUser(ctx *gin.Context) {
   req := &GetUserRequest{}
   if err := ctx.BindQuery(req); err != nil {
       return
   }
   user, err := uc.UserService.GetUser(ctx, req.UserID)
   if err != nil {
       ctx.JSON(400, gin.H{"error": err.Error()})
       return
   }
   ctx.JSON(200, &GetUserResponse{Username: user.Username})
}
  • 既然之前我们 service 的单元测试已经通过,这次我们就需要 mock 的是 service 层的接口 mockgen -source=./user.go -destination=../mock/user_service_mock.go -package=mock

  • 这里我将请求和返回的结构 如:GetUserRequest、GetUserResponse 放在了这里仅仅是为了方便展示代码

单元测试

基础代码非常简单,就是我们常见的,最重要的让我们来看看单元测试应该怎么写

工具方法

在编写实际单元测试之前,我们需要一些工具方法来帮助我们构建一些请求。

func createGetReqCtx(req interface{}, handlerFunc gin.HandlerFunc) (isSuccess bool, resp string) {
   w := httptest.NewRecorder()
   c, _ := gin.CreateTestContext(w)
   encode := structToURLValues(req).Encode()
   c.Request, _ = http.NewRequest("GET", "/?"+encode, nil)
   handlerFunc(c)
   return w.Code == http.StatusOK, w.Body.String()
}

func createPostReqCtx(req interface{}, handlerFunc gin.HandlerFunc) (isSuccess bool, resp string) {
   responseRecorder := httptest.NewRecorder()
   ctx, _ := gin.CreateTestContext(responseRecorder)
   body, _ := json.Marshal(req)
   ctx.Request, _ = http.NewRequest("POST", "/", bytes.NewBuffer(body))
   ctx.Request.Header.Set("Content-Type", "application/json")

handlerFunc(ctx)
   return responseRecorder.Code == http.StatusOK, responseRecorder.Body.String()
}

// 将结构体转换为 URL 参数
func structToURLValues(s interface{}) url.Values {
   v := reflect.ValueOf(s)
   if v.Kind() == reflect.Ptr {
       v = v.Elem()
   }
   t := v.Type()

values := url.Values{}
   for i := 0; i < t.NumField(); i++ {
       field := t.Field(i)
       tag := field.Tag.Get("form")
       if tag == "" {
           continue
       }

value := v.Field(i).Interface()
       values.Set(tag, valueToString(value))
   }

return values
}

// 由于 get 请求常常参数并不会特别复杂,通常的几种类型就应该可以包括,有需要可以继续添加
func valueToString(v interface{}) string {
   switch v := v.(type) {
   case int:
       return strconv.Itoa(v)
   case string:
       return v
   default:
       return ""
   }
}

既然我们不想启动路由,其实最关键的问题就在如何构建一个 gin.Context 来模拟正常的请求。

  • 通过 gin.CreateTestContext 创建一个我们需要模拟的 context

  • 通过 http.NewRequest 来创建我们需要的请求结构

单元测试

有了我们的工具方法,那么编写单元测试的时候就非常方便了,mock 方法和之前类似,剩下要调用对应的方法就可以了。并且这里可以复用我们已经在原有程序中使用的 请求结构 如 GetUserRequest 这样就可以不需要重新劳动了。

package controller

import (
   "fmt"
   "testing"

"github.com/golang/mock/gomock"
   "github.com/stretchr/testify/assert"
   "go-demo/m/unit-test/entity"
   "go-demo/m/unit-test/mock"
)

func TestUserController_AddUser(t *testing.T) {
   ctl := gomock.NewController(t)
   defer ctl.Finish()

req := &AddUserRequest{Username: "LinkinStar"}
   mockUserService := mock.NewMockUserService(ctl)
   mockUserService.EXPECT().AddUser(gomock.Any(), gomock.Any()).Return(nil)

userController := NewUserController(mockUserService)

success, resp := createPostReqCtx(req, userController.AddUser)
   assert.True(t, success)
   fmt.Println(resp)
}

func TestUserController_GetUser(t *testing.T) {
   ctl := gomock.NewController(t)
   defer ctl.Finish()

req := &GetUserRequest{UserID: 1}
   user := &entity.User{Username: "LinkinStar"}
   mockUserService := mock.NewMockUserService(ctl)
   mockUserService.EXPECT().GetUser(gomock.Any(), gomock.Any()).Return(user, nil)

userController := NewUserController(mockUserService)

success, resp := createGetReqCtx(req, userController.GetUser)
   assert.True(t, success)
   fmt.Println(resp)
}

可以看到测试方法如出一辙,再详细的话只需要对请求的返回值做解析然后进行断言即可。

问题

当然以上述方式来实现单元测试的话,是会遗漏一些问题,毕竟偷懒是要有代价的。

  • 路由路径的问题:可以看到上述的单元测试中并没有注册对应的 url 地址,那么实际中可能会由于代码路由的书写错误而导致 404 的情况

  • 请求结构字段错误:由于我们复用了原有代码中的请求结构,即使单词拼写错误依然能成功,因为两边都一样错,所以即使字段名称与接口文档不一致也无法发现。

针对这两个问题,我觉得可以由更加上层的测试来保证,由于这里仅仅是单元测试,我觉得这些代价还是可以接受的。并且,如果是使用 swagger 生成文档的情况下,也能保证文档和代码的统一性。但在此还是要出来提个醒,毕竟实际问题我还是遇到过的。

优化点

当然,这里的举例还是过于简单,实际中的请求往往会比较复杂。

  • 实际场景往往一些请求需要鉴权,这个可以在根据实际你的鉴权方式在前面添加中间件统一来处理登录就可以

  • 其他类型的请求也是类似的如 PUT、DELETE 等

  • 当前只是简单的处理了正常的 200 HTTP Code 还会出现其他异常的情况也需要按实际接口进行处理

来源:https://segmentfault.com/a/1190000043525232

标签:Golang,单元测试,接口层
0
投稿

猜你喜欢

  • Mac OS下PHP环境搭建及PHP操作MySQL常用方法小结

    2024-05-08 10:16:31
  • python3.7 打包成exe程序方式(只需两行命令)

    2023-01-26 14:29:34
  • Python中eval带来的潜在风险代码分析

    2023-05-24 03:29:03
  • JS组件Form表单验证神器BootstrapValidator

    2024-05-10 14:08:59
  • MySQL如何查询当前正在运行的SQL语句

    2009-02-13 13:40:00
  • FrontPage XP中的设计技巧

    2008-07-17 10:49:00
  • Python中实现最小二乘法思路及实现代码

    2021-06-17 02:23:24
  • Python sklearn CountVectorizer使用详解

    2023-06-20 08:19:05
  • asp 隐藏并修改文件的最后修改时间

    2011-03-29 10:34:00
  • JavaScript中.min.js和.js文件的区别讲解

    2024-04-30 09:56:19
  • Python 确定多项式拟合/回归的阶数实例

    2023-08-26 23:54:58
  • 用VB生成DLL封装ASP代码一个例子:连接access数据库等

    2008-04-07 13:06:00
  • python抓取搜狗微信公众号文章

    2021-10-25 17:56:08
  • 设计师如何更有效拿到结果?

    2008-09-22 20:30:00
  • 向上不间断(无缝)滚动图片js代码

    2007-09-24 20:22:00
  • Python如何在DataFrame增加数值

    2021-07-23 12:56:29
  • 基于PyQt5制作一个PDF文件合并器

    2023-04-27 07:50:58
  • 解决python多行注释引发缩进错误的问题

    2022-02-08 04:19:53
  • 原生js实现波浪导航效果

    2024-04-17 10:06:25
  • nodejs微信公众号支付开发

    2024-05-11 09:17:11
  • asp之家 网络编程 m.aspxhome.com