Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support generating mock for interfaces with generics #128

Closed
pavleprica opened this issue Dec 13, 2023 · 10 comments · Fixed by #207
Closed

Support generating mock for interfaces with generics #128

pavleprica opened this issue Dec 13, 2023 · 10 comments · Fixed by #207
Labels
bug Something isn't working

Comments

@pavleprica
Copy link

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
Copy link
Contributor

tulzke commented Dec 14, 2023

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
Copy link
Author

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
Copy link

krak3n commented Dec 18, 2023

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.

@r-hang r-hang added the bug Something isn't working label Dec 19, 2023
@tra4less
Copy link
Contributor

tra4less commented Jan 8, 2024

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
Copy link
Author

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
Copy link

ahmetb commented Feb 1, 2024

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
Copy link

tkrause commented Feb 2, 2024

Seeing the same thing

@appxpy
Copy link

appxpy commented Feb 8, 2024

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

@jonnylangefeld
Copy link

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
Copy link

lrascao commented Mar 25, 2024

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 {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
10 participants