关于单元测试:Golang-单元测试-接口层

5次阅读

共计 4914 个字符,预计需要花费 13 分钟才能阅读完成。

上次咱们曾经搞定了逻辑层的单元测试,这次咱们来康康接口层的单元测试。接口层次要负责的就是申请的解决,最常见的就是 HTTP 申请的解决。

但针对 接口层 的单元测试其实是能够形形色色的。它并不像逻辑层和数据层一样的通用,对于它的测试往往有很多路能够走。

因为应用的 HTTP 框架不同,单元测试的实现形式则不同。既能够通过程序来模仿 HTTP 申请,也能够通过实在的 HTTP 申请来测试,通过借助内部的一些测试工具来实现。

所以本文只能给出一种思路,具体的实现形式还是要依据理论的框架来实现。

环境

本文以罕用的 gin 框架为例,应用一种集体比拟喜爱也非常简单的形式来实现单元测试。特点次要有:

  1. 不须要启动路由服务
  2. 复用已有的我的项目内的申请构造

代码

因为之前曾经贴过,所以 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)
}

能够看到测试方法一模一样,再具体的话只须要对申请的返回值做解析而后进行断言即可。

问题

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

  1. 路由门路的问题:能够看到上述的单元测试中并没有注册对应的 url 地址,那么理论中可能会因为代码路由的书写谬误而导致 404 的状况
  2. 申请构造字段谬误:因为咱们复用了原有代码中的申请构造,即便单词拼写错误仍然能胜利,因为两边都一样错,所以即便字段名称与接口文档不统一也无奈发现。

针对这两个问题,我感觉能够由更加下层的测试来保障,因为这里仅仅是单元测试,我感觉这些代价还是能够承受的。并且,如果是应用 swagger 生成文档的状况下,也能保障文档和代码的统一性。但在此还是要进去提个醒,毕竟理论问题我还是遇到过的。

优化点

当然,这里的举例还是过于简略,理论中的申请往往会比较复杂。

  1. 理论场景往往一些申请须要鉴权,这个能够在依据理论你的鉴权形式在后面增加中间件对立来解决登录就能够
  2. 其余类型的申请也是相似的如 PUT、DELETE 等
  3. 以后只是简略的解决了失常的 200 HTTP Code 还会呈现其余异样的状况也须要按理论接口进行解决

总结

通常从景象来说,这一层的测试往往发现的问题比拟少,是因为这一层的逻辑少,测试下来最常见的问题往往就是字段名称和限度条件不满足需要。所以其实从性价比的角度来说,独自对这层拿进去测试往往比拟低,故理论中见到的比拟少。

不过话又说回来了,本文的目标不仅仅是为了让你理解到能够这样写单元测试,其中应用的办法往往还能再某些时候让你复用 handler 的办法来保证系统的一致性。

那么,接口层的单元测试完结了,在下一篇,未来介绍无关单元测试的其余一些小技巧。

正文完
 0