golang / mock

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

Support statically typed arguments in recorder #328

Closed liamylian closed 4 years ago

liamylian commented 5 years ago

It would be nice to mock method with statically typed arguments, rather than using interface arguments. I find myself check argument types all the time when mock a method, that is annoying and inefficent.

codyoss commented 5 years ago

Hey @liamylian would you mind providing me a more concrete example code snippets?

codyoss commented 5 years ago

Closing due to lack of response. If you want to submit an example and reopen, feel free.

jcxplorer commented 4 years ago

I'm assuming what @liamylian meant, is that the generated code uses empty interfaces as argument types for the MockRecorder methods. Compare the argument types in the Mock and MockRecorder methods:

// GetUserRoles mocks base method
func (m *MockRepository) GetUserRoles(arg0 context.Context, arg1 string) ([]*role.UserRole, error) {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "GetUserRoles", arg0, arg1)
    ret0, _ := ret[0].([]*role.UserRole)
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// GetUserRoles indicates an expected call of GetUserRoles
func (mr *MockRepositoryMockRecorder) GetUserRoles(arg0, arg1 interface{}) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserRoles", reflect.TypeOf((*MockRepository)(nil).GetUserRoles), arg0, arg1)
}

In the MockRecorder methods, both arg0 and arg1 are empty interfaces, but it would be nice to have them as (arg0 context.Context, arg1 string) as in the real method (first method in the example).

Is there a specific reason for this? I'm assuming there probably is, but if not, I could take a look at submitting a PR.

jcxplorer commented 4 years ago

Hi @codyoss, any thoughts on what I commented above? Still willing to provide a PR if you think this is something that makes sense, and which has a good chance of getting merged!

codyoss commented 4 years ago

@jcxplorer Recorders can accept matchers is the reason. Example:

m.EXPECT(). GetUserRoles(gomock.Any(), gomock.Any()).Return(nil, nil)
segiddins commented 2 years ago

Now that go has support for generics, I think it would be possible to do this. For example, here's what I've prototyped out:


type CallOf[Params any, Return any] struct {
    t gomock.TestHelper
    *gomock.Call
}

func (c *CallOf[Params, Return]) Return(ret Return) *CallOf[Params, Return] {
    c.t.Helper()

    val := reflect.ValueOf(ret)
    var rets []any
    for i := 0; i < val.NumField(); i++ {
        field := val.Field(i).Interface()
        rets = append(rets, field)
    }
    c.Call.Return(rets...)
    return c
}

func (c *CallOf[Params, Return]) DoAndReturn(fn func(params Params) Return) *CallOf[Params, Return] {
    c.t.Helper()

    c.Call.DoAndReturn(fn)
    return c
}

type MatcherOf[T any] interface {
    Matches(actual T) bool
    fmt.Stringer
}

func ToGomock[T any](matcher MatcherOf[T]) gomock.Matcher {
    return &gomockMatcherOf[T]{matcher: matcher}
}

type gomockMatcherOf[T any] struct {
    matcher MatcherOf[T]
}

func (g *gomockMatcherOf[T]) Matches(x interface{}) bool {
    if val, ok := x.(T); ok {
        return g.matcher.Matches(val)
    }
    return false
}

func (g *gomockMatcherOf[T]) String() string {
    return g.matcher.String()
}

// Acquire indicates an expected call of Acquire.
func (mr *MockDistributedLockRepoMockRecorder) Acquire(ctx MatcherOf[context.Context], key MatcherOf[string]) *CallOf[
    struct {
        ctx MatcherOf[context.Context]
        key MatcherOf[string]
    }, struct {
        r1 *v4.Mutex
        r2 error
    }] {
    mr.mock.ctrl.T.Helper()
    call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Acquire", reflect.TypeOf((*MockDistributedLockRepo)(nil).Acquire), ToGomock(ctx), ToGomock(key))

    return &CallOf[
        struct {
            ctx MatcherOf[context.Context]
            key MatcherOf[string]
        }, struct {
            r1 *v4.Mutex
            r2 error
        }]{
        call,
    }
}```

This is a bit cumbersome since it wraps params and return values in structs (due to lack of variadic generics), but we could also have some code-genned tuple structs that would eliminate the wrapping.