uber-go / mock

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

Support generating mock for interfaces with generics #128

Open pavleprica opened 6 months ago

pavleprica commented 6 months ago

Hey team,

Hope you are doing well. Stumbled upon on this issue. In code as well as on the archived repo. To not copy paste too much, is this perhaps on the roadmap for you?

It seems that the previous team was on it, but didn't got to the latest release including it.

tulzke commented 6 months ago

Hi @pavleprica . This has already been implemented and is working.

Example from a real project:

Interface:

type Storage[V any] interface {
    Set(ctx context.Context, item V, rowVersion internal.RowVersion) error
    Delete(ctx context.Context, item V, rowVersion internal.RowVersion) error
}

Mock:

...
// MockStorage is a mock of Storage interface.
type MockStorage[V any] struct {
    ctrl     *gomock.Controller
    recorder *MockStorageMockRecorder[V]
}
...
// Set mocks base method.
func (m *MockStorage[V]) Set(ctx context.Context, item V, rowVersion internal.RowVersion) error {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "Set", ctx, item, rowVersion)
    ret0, _ := ret[0].(error)
    return ret0
}
...
pavleprica commented 6 months ago

Hey @tulzke, Thank you for your response!

But this doesn't seem like handling of generics by gomock? Perhaps I'm just not seeing the whole picture.

But this is a "custom" implementation of the interface to mock it "manually". Not like you can rely on the .EXPECT() methods.

Or the example you provided is from the generated code?

krak3n commented 6 months ago

I'm coming across a similar problem I think, I'm trying to create a mock for this interface (a connectrpc handler interface generated from protos):

type ServiceHandler interface {
    Set(context.Context, *connect.Request[package.SetRequest]) (*connect.Response[package.SetResponse], error)
}

mockgen errors with:

2023/12/18 11:58:16 Failed to format generated source code: cachetest/handler.go:40:84: missing ',' in type argument list (and 5 more errors)

The generated code included with the error:

// Code generated by MockGen. DO NOT EDIT.
// Source: REDACTED
//
// Generated by this command:
//    mockgen github.com/repo/path/package/packageconnect ServiceHandler
// Package cachetest is a generated GoMock package.
package cachetest

import (
        reflect "reflect"
        connect "connectrpc.com/connect"
        context "context"
        gomock "go.uber.org/mock/gomock"
)

// MockHandler is a mock of Handler interface.
type MockHandler struct {
        ctrl     *gomock.Controller
        recorder *MockHandlerMockRecorder
}

// MockHandlerMockRecorder is the mock recorder for MockHandler.
type MockHandlerMockRecorder struct {
        mock *MockHandler
}

// NewMockHandler creates a new mock instance.
func NewMockHandler(ctrl *gomock.Controller) *MockHandler {
        mock := &MockHandler{ctrl: ctrl}
        mock.recorder = &MockHandlerMockRecorder{mock}
        return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockHandler) EXPECT() *MockHandlerMockRecorder {
        return m.recorder
}

// Set mocks base method.
func (m *MockHandler) Set(arg0 context.Context, arg1 *connect.Request[github.com/repo/path/package.SetRequest]) (*connect.Response[github.com/repo/path/package.SetResponse], error) {
        m.ctrl.T.Helper()
        ret := m.ctrl.Call(m, "Set", arg0, arg1)
        ret0, _ := ret[0].(*connect.Response[github.com/repo/path/package.SetResponse])
        ret1, _ := ret[1].(error)
        return ret0, ret1
}

// Set indicates an expected call of Set.
func (mr *MockHandlerMockRecorder) Set(arg0, arg1 any) *gomock.Call {
        mr.mock.ctrl.T.Helper()
        return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockHandler)(nil).Set), arg0, arg1)
}

As you can see from the generated code it doesn't look like mockgen can resolve the generic type imports correctly and we get invalid code.

tra4less commented 5 months ago

I executed mockgen --source=ping.connect.go --destination=mock/ping.mock.go --package mock command in github.com/connectrpc/connect-go/internal/gen/connect/ping/v1/pingv1connect, and the result was

// Code generated by MockGen. DO NOT EDIT.
// Source: ping.connect.go
//
// Generated by this command:
//
//  mockgen --source=ping.connect.go --destination=mock/ping.mock.go --package mock
//

// Package mock is a generated GoMock package.
package mock

import (
    context "context"
    reflect "reflect"

    connect "connectrpc.com/connect"
    pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1"
    gomock "go.uber.org/mock/gomock"
)

// MockPingServiceClient is a mock of PingServiceClient interface.
type MockPingServiceClient struct {
    ctrl     *gomock.Controller
    recorder *MockPingServiceClientMockRecorder
}

// MockPingServiceClientMockRecorder is the mock recorder for MockPingServiceClient.
type MockPingServiceClientMockRecorder struct {
    mock *MockPingServiceClient
}

// NewMockPingServiceClient creates a new mock instance.
func NewMockPingServiceClient(ctrl *gomock.Controller) *MockPingServiceClient {
    mock := &MockPingServiceClient{ctrl: ctrl}
    mock.recorder = &MockPingServiceClientMockRecorder{mock}
    return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPingServiceClient) EXPECT() *MockPingServiceClientMockRecorder {
    return m.recorder
}

// CountUp mocks base method.
func (m *MockPingServiceClient) CountUp(arg0 context.Context, arg1 *connect.Request[pingv1.CountUpRequest]) (*connect.ServerStreamForClient[pingv1.CountUpResponse], error) {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "CountUp", arg0, arg1)
    ret0, _ := ret[0].(*connect.ServerStreamForClient[pingv1.CountUpResponse])
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// CountUp indicates an expected call of CountUp.
func (mr *MockPingServiceClientMockRecorder) CountUp(arg0, arg1 any) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountUp", reflect.TypeOf((*MockPingServiceClient)(nil).CountUp), arg0, arg1)
}

// CumSum mocks base method.
func (m *MockPingServiceClient) CumSum(arg0 context.Context) *connect.BidiStreamForClient[pingv1.CumSumRequest, pingv1.CumSumResponse] {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "CumSum", arg0)
    ret0, _ := ret[0].(*connect.BidiStreamForClient[pingv1.CumSumRequest, pingv1.CumSumResponse])
    return ret0
}

// CumSum indicates an expected call of CumSum.
func (mr *MockPingServiceClientMockRecorder) CumSum(arg0 any) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CumSum", reflect.TypeOf((*MockPingServiceClient)(nil).CumSum), arg0)
}

// Fail mocks base method.
func (m *MockPingServiceClient) Fail(arg0 context.Context, arg1 *connect.Request[pingv1.FailRequest]) (*connect.Response[pingv1.FailResponse], error) {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "Fail", arg0, arg1)
    ret0, _ := ret[0].(*connect.Response[pingv1.FailResponse])
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// Fail indicates an expected call of Fail.
func (mr *MockPingServiceClientMockRecorder) Fail(arg0, arg1 any) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fail", reflect.TypeOf((*MockPingServiceClient)(nil).Fail), arg0, arg1)
}

// Ping mocks base method.
func (m *MockPingServiceClient) Ping(arg0 context.Context, arg1 *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "Ping", arg0, arg1)
    ret0, _ := ret[0].(*connect.Response[pingv1.PingResponse])
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// Ping indicates an expected call of Ping.
func (mr *MockPingServiceClientMockRecorder) Ping(arg0, arg1 any) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockPingServiceClient)(nil).Ping), arg0, arg1)
}

// Sum mocks base method.
func (m *MockPingServiceClient) Sum(arg0 context.Context) *connect.ClientStreamForClient[pingv1.SumRequest, pingv1.SumResponse] {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "Sum", arg0)
    ret0, _ := ret[0].(*connect.ClientStreamForClient[pingv1.SumRequest, pingv1.SumResponse])
    return ret0
}

// Sum indicates an expected call of Sum.
func (mr *MockPingServiceClientMockRecorder) Sum(arg0 any) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sum", reflect.TypeOf((*MockPingServiceClient)(nil).Sum), arg0)
}

// MockPingServiceHandler is a mock of PingServiceHandler interface.
type MockPingServiceHandler struct {
    ctrl     *gomock.Controller
    recorder *MockPingServiceHandlerMockRecorder
}

// MockPingServiceHandlerMockRecorder is the mock recorder for MockPingServiceHandler.
type MockPingServiceHandlerMockRecorder struct {
    mock *MockPingServiceHandler
}

// NewMockPingServiceHandler creates a new mock instance.
func NewMockPingServiceHandler(ctrl *gomock.Controller) *MockPingServiceHandler {
    mock := &MockPingServiceHandler{ctrl: ctrl}
    mock.recorder = &MockPingServiceHandlerMockRecorder{mock}
    return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPingServiceHandler) EXPECT() *MockPingServiceHandlerMockRecorder {
    return m.recorder
}

// CountUp mocks base method.
func (m *MockPingServiceHandler) CountUp(arg0 context.Context, arg1 *connect.Request[pingv1.CountUpRequest], arg2 *connect.ServerStream[pingv1.CountUpResponse]) error {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "CountUp", arg0, arg1, arg2)
    ret0, _ := ret[0].(error)
    return ret0
}

// CountUp indicates an expected call of CountUp.
func (mr *MockPingServiceHandlerMockRecorder) CountUp(arg0, arg1, arg2 any) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountUp", reflect.TypeOf((*MockPingServiceHandler)(nil).CountUp), arg0, arg1, arg2)
}

// CumSum mocks base method.
func (m *MockPingServiceHandler) CumSum(arg0 context.Context, arg1 *connect.BidiStream[pingv1.CumSumRequest, pingv1.CumSumResponse]) error {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "CumSum", arg0, arg1)
    ret0, _ := ret[0].(error)
    return ret0
}

// CumSum indicates an expected call of CumSum.
func (mr *MockPingServiceHandlerMockRecorder) CumSum(arg0, arg1 any) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CumSum", reflect.TypeOf((*MockPingServiceHandler)(nil).CumSum), arg0, arg1)
}

// Fail mocks base method.
func (m *MockPingServiceHandler) Fail(arg0 context.Context, arg1 *connect.Request[pingv1.FailRequest]) (*connect.Response[pingv1.FailResponse], error) {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "Fail", arg0, arg1)
    ret0, _ := ret[0].(*connect.Response[pingv1.FailResponse])
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// Fail indicates an expected call of Fail.
func (mr *MockPingServiceHandlerMockRecorder) Fail(arg0, arg1 any) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fail", reflect.TypeOf((*MockPingServiceHandler)(nil).Fail), arg0, arg1)
}

// Ping mocks base method.
func (m *MockPingServiceHandler) Ping(arg0 context.Context, arg1 *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "Ping", arg0, arg1)
    ret0, _ := ret[0].(*connect.Response[pingv1.PingResponse])
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// Ping indicates an expected call of Ping.
func (mr *MockPingServiceHandlerMockRecorder) Ping(arg0, arg1 any) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockPingServiceHandler)(nil).Ping), arg0, arg1)
}

// Sum mocks base method.
func (m *MockPingServiceHandler) Sum(arg0 context.Context, arg1 *connect.ClientStream[pingv1.SumRequest]) (*connect.Response[pingv1.SumResponse], error) {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "Sum", arg0, arg1)
    ret0, _ := ret[0].(*connect.Response[pingv1.SumResponse])
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// Sum indicates an expected call of Sum.
func (mr *MockPingServiceHandlerMockRecorder) Sum(arg0, arg1 any) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sum", reflect.TypeOf((*MockPingServiceHandler)(nil).Sum), arg0, arg1)
}
pavleprica commented 5 months ago

Hey there @tra4less,

I generally replicated the issue from the archived repository. I ran into it, while running into my own problem, which I presume might be related to the one.

But, to replicate my issue, you can use this code. A bit simplified, but should do the trick

ahmetb commented 5 months ago

Seeing exactly the same issue. In my case, the generated code has full package/module paths in the generics qualifiers:

// Code generated by MockGen. DO NOT EDIT.
// Source: golang.linkedin.com/config-publish-api/config-publish-api-go/generated/com/linkedin/config/compiledConfigDescriptors (interfaces: Client)
...

func (m *MockClient) Create(arg0 *config.CompiledConfigDescriptor) (*common.CreatedEntity[*golang.linkedin.com/config-publish-api/config-publish-api-go/generated/com/linkedin/config/compiledConfigDescriptors.CompiledConfigDescriptors_ComplexKey], error) {

...

func (m *MockClient) CreateWithContext(arg0 context.Context, arg1 *config.CompiledConfigDescriptor) (*common.CreatedEntity[*golang.linkedin.com/config-publish-api/config-publish-api-go/generated/com/linkedin/config/compiledConfigDescriptors.CompiledConfigDescriptors_ComplexKey], error) {

which looks like obvious syntax errors.

tkrause commented 5 months ago

Seeing the same thing

appxpy commented 4 months ago

Same thing, syntax error on generating mocks for interfaces with embedded generics.

jonnylangefeld commented 4 months ago

My issues were the same as described by @krak3n and @ahmetb. I'm using connectrpc and saw the full module names in the generated code, creating the syntax errors.

But @tra4less's solution of using mockgen source mode worked for me. I ran this command:

mockgen \
  -source=./pkg/proto/life/v1/lifev1connect/life.connect.go \
  -destination=./pkg/subsystem/mocks/mock_subsystem_service.go \
  -package=mocks github.com/jonnylangefeld/life/pkg/proto/life/v1/lifev1connect \
  SubsystemServiceClient

The generated code now had an import like this:

lifev1 "github.com/jonnylangefeld/life/pkg/proto/life/v1"

And the endpoints correctly used the named import in the generics code:

// SayHello mocks base method.
func (m *MockHelloServiceClient) SayHello(arg0 context.Context, arg1 *connect.Request[lifev1.SayHelloRequest]) (*connect.Response[lifev1.SayHelloResponse], error) {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "SayHello", arg0, arg1)
    ret0, _ := ret[0].(*connect.Response[lifev1.SayHelloResponse])
    ret1, _ := ret[1].(error)
    return ret0, ret1
}
lrascao commented 3 months ago

Also ran into this issue in which mockgen source mode doesn't help, here's a slim repro: https://play.golang.com/p/VPmHL06LTPH

generated code is:

// Set mocks base method.
func (m *MockSample) Set(arg0 context.Context, arg1 serializable) error {

when it should be:

// Set mocks base method.
func (m *MockSample) Set(arg0 context.Context, arg1 serializable[any]) error {