Testing 模拟Golang中的函数以测试我的http路由

Testing 模拟Golang中的函数以测试我的http路由,testing,go,mocking,go-gin,Testing,Go,Mocking,Go Gin,我完全搞不懂如何在不使用任何附加包(如golang/mock)的情况下模拟函数。我只是想学习如何做到这一点,但找不到多少像样的在线资源 本质上,我解释了如何使用接口来模拟事物 因此,我重新编写了我想要测试的函数。该函数只是将一些数据插入到数据存储中。我的测试还可以——我可以直接模拟函数 我遇到的问题是在我试图测试的http路由中模拟它。我正在使用Gin框架 我的路由器(简化)如下所示: func SetupRouter() *gin.Engine { r := gin.Default(

我完全搞不懂如何在不使用任何附加包(如golang/mock)的情况下模拟函数。我只是想学习如何做到这一点,但找不到多少像样的在线资源

本质上,我解释了如何使用接口来模拟事物

因此,我重新编写了我想要测试的函数。该函数只是将一些数据插入到数据存储中。我的测试还可以——我可以直接模拟函数

我遇到的问题是在我试图测试的http路由中模拟它。我正在使用Gin框架

我的路由器(简化)如下所示:

func SetupRouter() *gin.Engine {

    r := gin.Default()
    r.Use(gin.Logger())
    r.Use(gin.Recovery())

    v1 := r.Group("v1")
    v1.PATCH("operations/:id", controllers.UpdateOperation)
}
func (oi OperationInfoer) FindAndCompleteOp(id string, report Report) error {
    ctx := context.Background()
    q := datastore.NewQuery("Operation").
        Filter("Unique_Id =", id).
        Limit(1)

    var ops []Operation

    if ts, err := db.Datastore.GetAll(ctx, q, &ops); err == nil {
        {
            if len(ops) > 0 {
                ops[0].Id = ts[0].ID()
                ops[0].Complete = true

                // Do stuff

                _, err := db.Datastore.Put(ctx, key, &o)
                if err == nil {
                  log.Print("OPERATION COMPLETED")
                }
            }
        }
    }

    err := errors.New("Not found")
    return err
}

func FindAndCompleteOperation(ri OperationInfoer, id string, report Report) error {
    return ri.FindAndCompleteOp(id, report)
}

type OperationInfoer struct{}
m.FindAndCompleteOperation = func(string, m.Report) error {
  return nil
}
它调用UpdateOperation函数:

func UpdateOperation(c *gin.Context) {
    id := c.Param("id")
    r := m.Response{}

    str := m.OperationInfoer{}
    err := m.FindAndCompleteOperation(str, id, r.Report)

    if err == nil {
      c.JSON(200, gin.H{
          "message": "Operation completed",
      })
    }
}
因此,我需要模拟FindAndCompleteOperation()函数。

主要(简化)功能如下所示:

func SetupRouter() *gin.Engine {

    r := gin.Default()
    r.Use(gin.Logger())
    r.Use(gin.Recovery())

    v1 := r.Group("v1")
    v1.PATCH("operations/:id", controllers.UpdateOperation)
}
func (oi OperationInfoer) FindAndCompleteOp(id string, report Report) error {
    ctx := context.Background()
    q := datastore.NewQuery("Operation").
        Filter("Unique_Id =", id).
        Limit(1)

    var ops []Operation

    if ts, err := db.Datastore.GetAll(ctx, q, &ops); err == nil {
        {
            if len(ops) > 0 {
                ops[0].Id = ts[0].ID()
                ops[0].Complete = true

                // Do stuff

                _, err := db.Datastore.Put(ctx, key, &o)
                if err == nil {
                  log.Print("OPERATION COMPLETED")
                }
            }
        }
    }

    err := errors.New("Not found")
    return err
}

func FindAndCompleteOperation(ri OperationInfoer, id string, report Report) error {
    return ri.FindAndCompleteOp(id, report)
}

type OperationInfoer struct{}
m.FindAndCompleteOperation = func(string, m.Report) error {
  return nil
}
为了测试更新操作的路由,我有如下内容:

FIt("Return 200, updates operation", func() {
    testRouter := SetupRouter()

    param := make(url.Values)
    param["access_token"] = []string{public_token}

    report := m.Report{}
    report.Success = true
    report.Output = "my output"

    jsonStr, _ := json.Marshal(report)
    req, _ := http.NewRequest("PATCH", "/v1/operations/123?"+param.Encode(), bytes.NewBuffer(jsonStr))

    resp := httptest.NewRecorder()
    testRouter.ServeHTTP(resp, req)

    Expect(resp.Code).To(Equal(200))

    o := FakeResponse{}
    json.NewDecoder(resp.Body).Decode(&o)
    Expect(o.Message).To(Equal("Operation completed"))
})
起初,我尝试了一些欺骗,只是尝试了以下方式:

func SetupRouter() *gin.Engine {

    r := gin.Default()
    r.Use(gin.Logger())
    r.Use(gin.Recovery())

    v1 := r.Group("v1")
    v1.PATCH("operations/:id", controllers.UpdateOperation)
}
func (oi OperationInfoer) FindAndCompleteOp(id string, report Report) error {
    ctx := context.Background()
    q := datastore.NewQuery("Operation").
        Filter("Unique_Id =", id).
        Limit(1)

    var ops []Operation

    if ts, err := db.Datastore.GetAll(ctx, q, &ops); err == nil {
        {
            if len(ops) > 0 {
                ops[0].Id = ts[0].ID()
                ops[0].Complete = true

                // Do stuff

                _, err := db.Datastore.Put(ctx, key, &o)
                if err == nil {
                  log.Print("OPERATION COMPLETED")
                }
            }
        }
    }

    err := errors.New("Not found")
    return err
}

func FindAndCompleteOperation(ri OperationInfoer, id string, report Report) error {
    return ri.FindAndCompleteOp(id, report)
}

type OperationInfoer struct{}
m.FindAndCompleteOperation = func(string, m.Report) error {
  return nil
}
但这会影响所有其他测试等


我希望有人能简单地解释一下模拟FindAndCompleteOperation函数的最佳方式,这样我就可以测试路由,而不必依赖数据存储等。

如果使用处理程序中无法模拟的全局变量,则无法模拟。要么全局变量是可模拟的(即声明为接口类型的变量),要么需要使用依赖项注入

func (oi OperationInfoer) FindAndCompleteOp(id string, report Report) error {...}
看起来像一个结构的方法,所以您至少应该能够将该结构注入到处理程序中

type OperationInfoer interface {
   FindAndCompleteOp(id string, report Report) error 
} 

type ConcreteOperationInfoer struct { /* actual implementation */ }

func UpdateOperation(oi OperationInfoer) func(c *gin.Context) {
    return func (c *gin.Context){
        // the code
    }
}
然后,模拟在您的测试中变得轻而易举:

UpdateOperation(mockOperationInfoer)(ginContext)
可以使用结构而不是闭包

type UpdateOperationHandler struct {
    Oi OperationInfoer
}
func (h UpdateOperationHandler ) UpdateOperation (c *gin.Context) {
    h.Oi.FindAndCompleteOp(/* code */ )
}

对于一个类似的问题,我有另一个相关的、信息更丰富的答案,但以下是针对您的具体场景的答案:

更新您的
SetupRouter()
函数以获取一个函数,该函数可以是真正的
find和completeOperation
函数,也可以是存根函数:

func UpdateOperation(c *gin.Context) {
    id := c.Param("id")
    r := m.Response{}

    str := m.OperationInfoer{}
    err := m.FindAndCompleteOperation(str, id, r.Report)

    if err == nil {
      c.JSON(200, gin.H{
          "message": "Operation completed",
      })
    }
}

测试/模拟示例

package main

import (
    "encoding/json"
    "net/http/httptest"
    "strings"
    "testing"
)

func TestUpdateRoute(t *testing.T) {
    // build findAndComplete stub
    var callCount int
    var lastInfoer OperationInfoer
    var lastID string
    var lastReport Report
    stub := func(s OperationInfoer, id string, report Report) error {
        callCount++
        lastInfoer = s
        lastID = id
        lastReport = report
        return nil // or `fmt.Errorf("Err msg")` if you want to test fault path
    }

    // invoke endpoint
    w := httptest.NewRecorder()
    r := httptest.NewRequest(
        "PATCH",
        "/v1/id_value",
        strings.NewReader(""),
    )
    SetupRouter(stub).ServeHTTP(w, r)

    // check that the stub was invoked correctly
    if callCount != 1 {
        t.Fatal("Wanted 1 call; got", callCount)
    }
    if lastInfoer != (OperationInfoer{}) {
        t.Fatalf("Wanted %v; got %v", OperationInfoer{}, lastInfoer)
    }
    if lastID != "id_value" {
        t.Fatalf("Wanted 'id_value'; got '%s'", lastID)
    }
    if lastReport != (Report{}) {
        t.Fatalf("Wanted %v; got %v", Report{}, lastReport)
    }

    // check that the correct response was returned
    if w.Code != 200 {
        t.Fatal("Wanted HTTP 200; got HTTP", w.Code)
    }

    var body map[string]string
    if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
        t.Fatal("Unexpected error:", err)
    }
    if body["message"] != "Operation completed" {
        t.Fatal("Wanted 'Operation completed'; got %s", body["message"])
    }
}

谢谢,在我当前的测试中,是否有可能对此进行扩展。我仍然不确定这一切是如何结合在一起的。JTLDR;更改您的代码使其“可模仿”,因为显示给我的代码不是。如果在创建处理程序时不注入作为接口的依赖项,则无法模拟任何内容。它与其他静态类型语言没有什么不同。在Java中也是一样,你的方法应该接受接口作为参数,而不是具体的类型,你不应该依赖于没有注入的类型。但这正是我需要帮助的地方。我不知道如何实际实施。如果我知道的话,我就不会浪费时间问这个问题了。而且,在这个函数中不可能使用结构的闭包或方法。或者至少,我不认为这是可能的。@Jennybrount你是对的,你不能在UpdateOperation函数中使用闭包或方法,所以我们需要将该函数变成闭包,以便我们可以参数化它。看我的答案。我在这里有一个类似问题的答案:长话短说:您需要注入FindAndCompleteOperation()函数来模拟它。下面是另一种方法,它有一个完整的示例,可以清楚地说明如何用Go编写可测试代码:可能的重复。你能把测试也放在那里吗?@Jennybunt test补充道。慢慢来,这让我崩溃了。我不知道为什么:(在我的例子中,我需要测试整个路由。我现在已经更新了我的函数,但仍然看不到我需要在我的示例中测试什么。@Jennybrount不用担心。要测试整个路由,您需要参数化SetupRouter()函数将
findAndComplete
闭包作为一个参数。从那里,您只需调用
gin.Engine
ServeHTTP()
方法和适当的请求(可能还有一些模拟的
http.ResponseWriter
)。
ServeHTTP()
允许引擎实现标准库的
http.Handler
接口,因此您应该能够通过谷歌搜索“golang中的http测试”来找到更多关于测试的信息——这里您不局限于gin特定的示例。@JennyBlunt我更新了我的示例。您可以复制/粘贴此示例,它将运行。