调用服务接口的单元测试 gin gonnic 控制器

分享于2022年07月16日 go go-gin 问答
【问题标题】:Unit Testing gin gonnic controller which calls service interface调用服务接口的单元测试 gin gonnic 控制器
【发布时间】:2022-06-30 12:11:43
【问题描述】:

我在 go 中有一个简单的 rest api 项目。以下是我的 main.go 文件。

package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    api := initApi()
    r.GET("/hi", api.SayHi)
    r.Run()
}

initapi 函数返回一个 hiapi 结构。

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

import (
    "rest-api/api"
    "rest-api/repo"
    "rest-api/service"
)

// Injectors from wire.go:

func initApi() api.HiAPI {
    hiRepo := repo.NewRepo()
    hiService := service.NewService(hiRepo)
    hiAPI := api.NewApi(hiService)
    return hiAPI
}

这个结构体保存在嵌入服务层的api包中。

package api

import (
    "net/http"
    "rest-api/service"

    "github.com/gin-gonic/gin"
)

type IApi interface {
    SayHi(c *gin.Context)
}
type HiAPI struct {
    HiService service.HiService
}

func NewApi(h service.HiService) HiAPI {
    return HiAPI{HiService: h}
}

func (h *HiAPI) SayHi(c *gin.Context) {
    m := h.HiService.SayHi()
    c.JSON(http.StatusOK, gin.H{"message": m})
}

如您所见,api 包调用的是服务包,而服务包调用的是存储库包。都是通过接口调用的。现在我的问题是如何模拟对服务层的调用来测试控制器?

我尝试了以下代码进行测试。

func TestHi(t *testing.T){
    w := httptest.NewRecorder()
    // c, _ := gin.CreateTestContext(w)
    mockeService := new(MockService)
    c, _ := gin.CreateTestContext(w)
    IApi.SayHi(c)
    mockeService.On("SayHi").Return( "hello")
    assert.Equal(t, 200, w.Code)
    assert.Equal(t,"hello",w.Body.String() )
    var got gin.H
    err := json.Unmarshal(w.Body.Bytes(), &got)
    if err != nil {
        t.Fatal(err)
    }
    // assert.Equal(t, want, got) 
}

但我在 IApi.SayHi(c) 行出现错误,错误是 not enough arguments in call to IApi.SayHi have (*"rest-api/vendor/github.com/gin-gonic/gin".Context) want (IApi, *"github.com/gin-gonic/gin".Context)compilerWrongArgCount

如何修复此错误并为服务层添加模拟?


【解决方案1】:

抱歉,有点晚了。我没有使用 wire 来做我的DI,但我有一个类似的流程,你有一个 controller 方法调用一个 service 方法使用 go-gin ,并且需要做一些模拟来做测试我的 controller

我会使用 gin.Context 来设置我的依赖项而不是 main.go

package main


import (
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    api := initApi()
    // Set all service dependencies here
    r.Use(util.SetDependencies)
    r.GET("/hi", api.SayHi)
    r.Run()
}

我的 util 包裹:

package util


// Set all service dependencies thru Gin Context here
func SetDependencies(c *gin.Context) {
    // This is the concrete service struct
    var hiService hiService.HiService 
    c.Set("hi_service", &hiService)
}

// Get dependency thru Gin Context - IHiService is your HiService's interface
// you may need to create more "getter" functions if you have more service dependencies
func GetHiServiceDependency(c *gin.Context) (hiService.IHiService, error) {
    service, exists := c.Get("hi_service")
    if !exists {
        return nil, errors.New("hi_service not set")
    }
    return service.(hiService.HiService), nil
}

然后在您的 api 包(控制器)上:

// Omitted other code for brevity
func (h *HiAPI) SayHi(c *gin.Context) {
    hiService, err := util.GetHiServiceDependency(c)
    if err != nil {
        response := CustomErrorResponse{
            Status: http.StatusInternalServerError,
            Error:  err.Error(),
        }
        c.JSON(http.StatusInternalServerError, response)
        return
    }
    m := hiService.SayHi()
    c.JSON(http.StatusOK, gin.H{"message": m})
}

然后在你的测试代码上,你可以这样模拟它:

// Create your mock HiService following IHiService interface 
// (assuming this is how your IHiService interface looks like)
type MockHiService struct {}

func (m *MockHiService) SayHi() string {
    return "Hi"
}

func TestHi(t *testing.T){
    gin.SetMode(gin.TestMode)

    w := httptest.NewRecorder()
    c, _ := gin.CreateTestContext(w)

    // Set it like in the util.SetDependencies I mentioned
    var mockHiService MockHiService
    c.Set("hi_service", &mockHiService)
    
    SayHi(c)

    var got gin.H
    err := json.Unmarshal(w.Body.Bytes(), &got)
    if err != nil {
        t.Fatal(err)
    }
    // Test as needed
    // assert.Equal(t, want, got) 
}

我也是从另一个 SO 答案中得到这个想法的,如果你想要更多想法,也可以尝试参考它。

【讨论】: