在Golang中模拟函数以测试我的HTTP路由。

huangapple go评论127阅读模式
英文:

Mocking functions in Golang to test my http routes

问题

我完全困惑了,想弄清楚如何模拟一个函数,而不使用任何额外的包,比如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)
}

它调用了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 (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{}

为了测试更新操作的路由,我有以下代码:

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"))
})

最初,我试图作弊一下,只是尝试这样做:

m.FindAndCompleteOperation = func(string, m.Report) error {
    return nil
}

但这会影响到所有其他的测试等。

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

英文:

I'm totally confused figuring out how I can mock a function, without using any additional packages like golang/mock. I'm just trying to learn how to do so but can't find many decent online resources.

Essentially, I followed this excellent article that explains how to use an interface to mock things.

As so, I've re-written the function I wanted to test. The function just inserts some data into datastore. My tests for that are ok - I can mock the function directly.

The issue I'm having is mocking it 'within' an http route I'm trying to test. Am using the Gin framework.

My router (simplified) looks like this:

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)
}

Which calls the UpdateOperation function:

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",
	  })
	}
}

So, I need to mock the FindAndCompleteOperation() function.

The main (simplified) functions looks like this:

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{}

To test the route that updates the operation, I have something like so:

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"))
})

Originally, I tried to cheat a bit and just tried something like this:

m.FindAndCompleteOperation = func(string, m.Report) error {
  return nil
}

But that affects all the other tests etc.

I'm hoping someone can explain simply what the best way to mock the FindAndCompleteOperation function so I can test the routes, without relying on datastore etc.

答案1

得分: 2

我有另一个相关且更具信息性的答案,针对类似问题的答案在这里:链接,但是这里是针对你特定情况的答案:

更新你的 SetupRouter() 函数,使其接受一个函数作为参数,该函数可以是真正的 FindAndCompleteOperation 函数,也可以是一个存根函数:

Playground

package main

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

// m.Response.Report
type Report struct {
	// ...
}

// m.OperationInfoer
type OperationInfoer struct {
	// ...
}

type findAndComplete func(s OperationInfoer, id string, report Report) error

func FindAndCompleteOperation(OperationInfoer, string, Report) error {
	// ...
	return nil
}

func SetupRouter(f findAndComplete) *gin.Engine {
	r := gin.Default()
	r.Group("v1").PATCH("/:id", func(c *gin.Context) {
		if f(OperationInfoer{}, c.Param("id"), Report{}) == nil {
			c.JSON(200, gin.H{"message": "Operation completed"})
		}
	})
	return r
}

func main() {
	r := SetupRouter(FindAndCompleteOperation)
	if err := r.Run(":8080"); err != nil {
		panic(err)
	}
}

测试/模拟示例

package main

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

func TestUpdateRoute(t *testing.T) {
	// 构建 findAndComplete 存根
	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 // 或者 `fmt.Errorf("Err msg")`,如果你想测试错误路径
	}

	// 调用端点
	w := httptest.NewRecorder()
	r := httptest.NewRequest(
		"PATCH",
		"/v1/id_value",
		strings.NewReader(""),
	)
	SetupRouter(stub).ServeHTTP(w, r)

	// 检查存根是否被正确调用
	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)
	}

	// 检查是否返回了正确的响应
	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", body["message"])
	}
}
英文:

I have another relevant, more informative answer to a similar question here, but here's an answer for your specific scenario:

Update your SetupRouter() function to take a function that can either be the real FindAndCompleteOperation function or a stub function:

Playground

package main
import "github.com/gin-gonic/gin"
// m.Response.Report
type Report struct {
// ...
}
// m.OperationInfoer
type OperationInfoer struct {
// ...
}
type findAndComplete func(s OperationInfoer, id string, report Report) error
func FindAndCompleteOperation(OperationInfoer, string, Report) error {
// ...
return nil
}
func SetupRouter(f findAndComplete) *gin.Engine {
r := gin.Default()
r.Group("v1").PATCH("/:id", func(c *gin.Context) {
if f(OperationInfoer{}, c.Param("id"), Report{}) == nil {
c.JSON(200, gin.H{"message": "Operation completed"})
}
})
return r
}
func main() {
r := SetupRouter(FindAndCompleteOperation)
if err := r.Run(":8080"); err != nil {
panic(err)
}
}

Test/mocking example

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"])
}
}

答案2

得分: 1

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

看起来func (oi OperationInfoer) FindAndCompleteOp(id string, report Report) error是一个结构体的方法,所以你应该能够将该结构体注入到处理程序中。

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

type ConcreteOperationInfoer struct { /* 实际实现 */ }

func UpdateOperation(oi OperationInfoer) func(c *gin.Context) {
    return func (c *gin.Context){
        // 代码
    }
}

然后在你的测试中,模拟就变得非常简单:

UpdateOperation(mockOperationInfoer)(ginContext)

你可以使用结构体代替闭包:

type UpdateOperationHandler struct {
    Oi OperationInfoer
}
func (h UpdateOperationHandler ) UpdateOperation (c *gin.Context) {
    h.Oi.FindAndCompleteOp(/* 代码 */ )
}
英文:

You can't mock if you use globals that can't be mocked in an handler. Either your globals are mockable (i.e. declared as variables of interface type) or you need to use dependency injection.

func (oi OperationInfoer) FindAndCompleteOp(id string, report Report) error {...}

looks like a method of a struct, so you should be able to inject that struct into an handler, at the very least.

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
}
}

then mocking becomes a breeze in your tests :

UpdateOperation(mockOperationInfoer)(ginContext)

You can use a struct instead of closures

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

huangapple
  • 本文由 发表于 2016年9月27日 19:21:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/39723345.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定