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