Golang中的模拟功能以测试我的http路由

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.

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

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