验证码: 看不清楚,换一张 查询 注册会员,免验证
  • {{ basic.site_slogan }}
  • 打开微信扫一扫,
    您还可以在这里找到我们哟

    关注我们

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

阅读:958 来源:乙速云 作者:代码code

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

      环境

      本文以常用的 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 还会出现其他异常的情况也需要按实际接口进行处理

    分享到:
    *特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: hlamps#outlook.com (#换成@)。
    相关文章
    {{ v.title }}
    {{ v.description||(cleanHtml(v.content)).substr(0,100)+'···' }}
    你可能感兴趣
    推荐阅读 更多>