golang / mock

GoMock is a mocking framework for the Go programming language.
Apache License 2.0
9.3k stars 610 forks source link

Allow gomock to mock functions, not just interfaces #519

Closed cwmos closed 3 years ago

cwmos commented 3 years ago

Requested feature

Currently, if I run mockgen on a file, it only seems to generate mock versions of interfaces. I propose it should also generate mock versions of functions. So if my file contains a line like

type TestFunc func() int

Then mockgen should be able to generate a mocked instance of that function. So I should be able to write something like:

ctrl := gomock.NewController(t)
m := NewMockTestFunc(ctrl)
m.EXPECT().Return(101)
m.EXPECT().Return(102)
m.Func()() // Returns 101
m.Func()() // Returns 102

Why the feature is needed

Sometimes, dependencies to code are not interfaces but simple function callbacks. It would be nice if I can use gomock to mock these function callbacks. I understand a work around would be to wrap such a callback into an interface, but it would be nice if that was not needed.

codyoss commented 3 years ago

Hey @cwmos thanks for the request.

Unfortunately I don't know how this could work. Interfaces work because we can provide an alternative implementation to satisfy the interface at test time vs run time. I am sure we could generate a function that matches a signature but your code would still depend on the real function. If a function is complex enough where it needs to be mocked I would say instead:

  1. Accept interfaces into that function so you can mock the dependency.
  2. Create a small interface for your function.
cwmos commented 3 years ago

Thanks for your quick reply @codyoss .

I have read your reply several times, but I don't really get it. I agree that the struct you get from a call to NewMockXXX directly satisfies the mocked interface. I also agree that this cannot work for functions. But I think this can be solved by having the struct you get from a call to NewMockXXX have a function that returns the mocked function. I called this function "Func" in my original proposal. Maybe it should not even be a function but just a direct member of the returned struct (replace "Func()" with "Func" in my proposal). Why can this not work?

Since I wrote my proposal have used gomock allot and I still think it would be really nice if gomock could mock functions. I have used your alternative proposal. It just seems to require many lines of boilerplate code in order to use an interface with one function instead of function directly.

codyoss commented 3 years ago

If you don't have an interface how would you switch out your function at test time vs run time? Here are a few different patterns I have seen people use for mocking functions. In these examples I am "mocking" away time.Now():

package mockme

import (
    "testing"
    "time"
)

var fixedTime = time.Now()

// Option 1: var
var now func() time.Time = time.Now

func Foo() string {
    return now().String()
}

// In test code
func TestSomething(t *testing.T) {
    // reset after test
    defer func(o func() time.Time) { now = o }(now)
    // Return a static time
    now = func() time.Time { return fixedTime }
    got := Foo()
    // ...
}

// Option 2: refactor

func RefactoredFoo(t time.Time) string {
    return t.String()
}

// In test code
func TestSomething2(t *testing.T) {
    got := RefactoredFoo(fixedTime)
    // ...
}

// Option 3: struct wrapper
type Bar struct {
    nowFn func() time.Time
}

func (b *Bar) Foo() string {
    return b.now().String()
}

func (b *Bar) now() time.Time {
    if b == nil || b.nowFn == nil {
        return time.Now()
    }
    return b.nowFn()
}

// In test code
func TestSomething3(t *testing.T) {
    b := &Bar{
        nowFn: func() time.Time { return fixedTime },
    }
    got := b.Foo()
    // ...
}
cwmos commented 3 years ago

Thank @codyoss for clarifying your point! I think you give several examples on how test code can inject an alternative implementation of a function. My request is to allow gomock to provide the implementation of that function during testing.

Let me try to explain my use case by a complete example. Suppose I have production code where reading the current time is injected as a dependency:

package main

import "time"
import "fmt"

type TimeProvider interface {
    Now() time.Time
}

type FancySystem struct {
    timeProvider TimeProvider
    // ... other stuff
}

func (fs *FancySystem) DoStuff() {
    fmt.Printf("Time is %v\n",fs.timeProvider.Now())
}

// other functions on FancySystem

func NewFancySystem(timeProvider TimeProvider) *FancySystem {
    return &FancySystem{timeProvider}
}

type RealTimeProvider struct {}

func (rp RealTimeProvider) Now() time.Time {
    return time.Now()
}

var RealFancySystem = NewFancySystem(RealTimeProvider{})

To test the FancySystem, I would use something like this:

package main

import (
    "testing"
    "time"

    "github.com/golang/mock/gomock"
)

func TestCalc(t *testing.T) {
    ctrl := gomock.NewController(t)

    timeProvider := NewMockTimeProvider(ctrl)
    fancySystem := NewFancySystem(timeProvider)

    timeProvider.EXPECT().Now().Return(time.Time{})
    fancySystem.DoStuff()

    ctrl.Finish()
}

Now, this works fine, except than in this case, and many other cases, it is a bit overkill to use an interface when there is only one function in it. So the production code would rather want to look like this:

type TimeProvider func() time.Time

type FancySystem struct {
    timeProvider TimeProvider
}

func (fs *FancySystem) DoStuff() {
    fmt.Printf("Time is %v\n",fs.timeProvider())
}

func NewFancySystem(timeProvider TimeProvider) *FancySystem {
    return &FancySystem{timeProvider}
}

var RealFancySystem = NewFancySystem(time.Now)

And the test code like this:

func TestCalc(t *testing.T) {
    ctrl := gomock.NewController(t)

    timeProvider := NewMockTimeProvider(ctrl)
    fancySystem := NewFancySystem(timeProvider.Func)

    timeProvider.EXPECT().Return(time.Time{})
    fancySystem.DoStuff()

    ctrl.Finish()
}

Does it make sense?

codyoss commented 3 years ago

Does it make sense?

Yes that does. I was not thinking of the function as a type but just a package function. But I still do disagree and think it is still best to use an interface in these cases. You can look to the standard library and find single method interfaces. They are not frowned upon but encouraged! They allow great extensibility when writing code.