3

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.

Jenny Blunt
  • 1,576
  • 1
  • 18
  • 41
  • I have an answer to a similar question over here: http://stackoverflow.com/questions/19167970/mock-functions-in-go/19168875#19168875 Long story short: you need to inject your FindAndCompleteOperation() function in order to mock it. – weberc2 Sep 27 '16 at 15:40
  • Here's another approach with a full example that should make clear how it is possible to write testable code in Go: https://stackoverflow.com/a/48206430/828366 – Francesco Casula Jan 11 '18 at 14:23
  • Possible duplicate of [Mock functions in Go](https://stackoverflow.com/questions/19167970/mock-functions-in-go) – Francesco Casula Jan 11 '18 at 14:24

2 Answers2

2

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"])
    }
}
Community
  • 1
  • 1
weberc2
  • 7,423
  • 4
  • 41
  • 57
  • Could you put the test in there too? – Jenny Blunt Sep 27 '16 at 16:02
  • @JennyBlunt Test added. – weberc2 Sep 27 '16 at 16:09
  • Getting there slowly, this is breaking me. I don't know why :( In my case, I need to test the whole route. I've now updated my functions but still don't see what I need to put in the test in my example. – Jenny Blunt Sep 27 '16 at 16:23
  • @JennyBlunt No worries. To test your whole route, you'll need to parameterize your SetupRouter() function to take the `findAndComplete` closure as a parameter. From there you just invoke the `gin.Engine`'s `ServeHTTP()` method with the proper request (and probably some mock `http.ResponseWriter`). `ServeHTTP()` allows the engine to implement the standard library's `http.Handler` interface, so you should be able to find more information about testing by Googling "http testing in golang"--you're not restricted to gin-specific examples here. – weberc2 Sep 27 '16 at 16:28
  • If you're still struggling, I can put together a more case-specific example tonight. – weberc2 Sep 27 '16 at 16:30
  • That'd be great, thanks. Just cant put the pieces together. That makes sense though, I kinda thought there would be a simple way to stub that particular function so it returned whatever... Guess I'm spoiled by Ruby.. In the meantime, I will read up on general http testing. Thanks again. – Jenny Blunt Sep 27 '16 at 16:34
  • @JennyBlunt I'm guessing the hard part isn't creating the stub, but actually replacing the real call with the stub. This is hard because your code is tightly coupled. Ruby lets you test tightly coupled code via patching, but really you should try to avoid tightly coupled code in the first place (patching is a sign of poor architecture). – weberc2 Sep 27 '16 at 16:41
  • Yeah, it looks that way. And I have a function (just about to add to the above question) that sets up the routes. I can override this but then I'm not actually testing anything... – Jenny Blunt Sep 27 '16 at 16:46
  • Actually it's in there already - SetupRouter() *gin.Engine. My bad. – Jenny Blunt Sep 27 '16 at 16:47
  • Just found this but it's epic. https://dev.makeitsocial.com/blog/2015/12/04/golang-test-http/ – Jenny Blunt Sep 27 '16 at 17:03
  • 1
    @JennyBlunt I updated my example. You can copy/paste this and it will run. – weberc2 Sep 27 '16 at 17:24
  • Cool, it's starting to make some sense. OMG. Presumably I should change findAndComplete (which gets sent to the Setup fn) to an interface, rather than a function? That way I can add multiple routes to the setup and test them in similar ways?? – Jenny Blunt Sep 27 '16 at 18:07
  • @JennyBlunt Probably. Interfaces and closures serve similar purposes. If you have a lot of closures that close over similar state, you probably want an interface. Otherwise it's pretty much personal preference. – weberc2 Sep 27 '16 at 19:05
  • Ok thanks for the help. Still a bit unsure but you've helped a lot. J – Jenny Blunt Sep 27 '16 at 21:37
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/124356/discussion-between-weberc2-and-jenny-blunt). – weberc2 Sep 27 '16 at 22:09
1

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 */ )
}
mpm
  • 20,148
  • 7
  • 50
  • 55
  • Thanks, would it be possible to expand on this in the context of my current test. I'm still unsure how it all fits together. J – Jenny Blunt Sep 27 '16 at 15:05
  • 1
    TLDR; change your code to make it "mockable", because the piece of code showed me is not. You can't mock anything if you don't inject dependencies that are interfaces when creating your handlers. It's no different from other statically typed languages. In Java it would be exactly the same thing, your methods should accept interfaces as argument instead of concrete types and you should not rely on types that are not injected. – mpm Sep 27 '16 at 15:38
  • Ok - I understand this. But that is exactly what I need help with. I don't know how to actually implement. If I knew, I wouldn't have wasted the time asking the question. – Jenny Blunt Sep 27 '16 at 15:50
  • Also, it's not possible to use a closure or method of a struct in that gin function. Or at least, I don't see how that's possible. – Jenny Blunt Sep 27 '16 at 15:56
  • @JennyBlunt You're right that you can't use a closure or method *in* your UpdateOperation function, so we need to *make* that function into a closure so we can parameterize it. See my answer. – weberc2 Sep 27 '16 at 16:14