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


猜你喜欢
- 本文实例讲述了PHP操作MySQL中BLOB字段的方法。分享给大家供大家参考,具体如下:1、MySQL中BLOB字段类型BLOB类型的字段用
- 方法一:mylist = [1,2,2,2,2,3,3,3,4,4,4,4]myset = set(mylist)for item in m
- 在php中使用Xajax能够即时与数据库发生交互带给用户更好的体验主要的应用有网页的即时、不刷新的登录系统也可以利用于注册系统中即时验证用户
- 在tensorflow中,经常会遇到参数初始化问题,比如在训练自己的词向量时,需要对原始的embeddigs矩阵进行初始化,更一般的,在全连
- 建立完回归模型后,还需要验证咱们建立的模型是否合适,换句话说,就是咱们建立的模型是否真的能代表现有的因变量与自变量关系,这个验证标准一般就选
- 第一种是最传统的写法,用存储过程中的变量作为分页的乘数 代码如下:[c-sharp] view plaincopyprint?create
- 打开和关闭文件操作:os.Open()函数能够打开一个文件,返回一个*File和一个err。对得到的文件实例调用close()方法能够关闭文
- 本文实例讲述了Go语言的队列和堆栈实现方法。分享给大家供大家参考。具体如下:golang,其实我的实现是利用container/list包实
- 工作中,经常会有用python访问各种数据库的需求,比如从oracle读点配置文件或者往mysql写点结果信息之类的。这里列一下可能用到的各
- 背景堆是一种非常常用的数据结构,它能够支持在O(1)的时间复杂度获取到最大值(或最小值),因此我们经常在需要求最值的场景使用它。然而普通堆它
- 写在前面:前一段时间 kejun 给我们培训JavaScript的时候,在幻灯片上推荐了很多特别经典的文章,其中就有这一篇。读过之后感觉很不
- 1. 准备工作下载源码包wget http://python.org/ftp/python/2.7.3/Python-2.7.3.tar.b
- 1.在OpenCV中我们经常会遇到一个名字:Mask(掩膜)。很多函数都使用到它,那么这个Mask到底什么呢?2.如果我们想要裁剪图像中任意
- 一、分组原理核心:1、不论分组键是数组、列表、字典、Series、函数,只要其与待分组变量的轴长度一致都可以传入groupby进行分组。2、
- 概要在自然语言处理(NLP)领域,情感分析及分类是一项十分热门的任务。它的目标是从文本中提取出情感信息和意义,通常分为两类:正向情感和负向情
- 本文实例为大家分享了使用python3制作的表白神器,供大家参考,具体内容如下使用python3制作的小玩意,哪里需要改造的,望各位评论一下
- 自动化测试执行过程中,难免会有错误/异常出现,比如测试脚本没有发现对应元素,则会立刻抛出NoSuchElementException异常。这
- 解决方式一:import matplotlib#1. 获取matplotlibrc文件所在路径matplotlib.matplotlib_f
- 在使用python编程过程中,我们往往需要借助字典来提高编程效率。同时为了调试方便,我们希望将某些变量保存为中间文件。例如,在协同过滤算法中
- 将源目录的图片用MD5命名并设定目标目录尝试了一下用 python 实现了一个生成密码的程序。感觉还是比较好上手的。但是那个程序还是非常简单