vektra / mockery

A mock code autogenerator for Go
https://vektra.github.io/mockery/
BSD 3-Clause "New" or "Revised" License
6k stars 405 forks source link

Option to replace a generic type parameter with a specific type #747

Closed DustinJSilk closed 7 months ago

DustinJSilk commented 8 months ago

Description

When using an interface that describes a method that returns a clone of the struct, the generated mocks cannot be used.

Take this interface for example:

type Repository[T any] interface {
  Clone() T
}

This can be implemented and used normally with this struct (note, no generics used here):

type repository struct {}

func (r *repository) Clone() *repository {
  return &repository{}
}

This struct can successfully be injected into a function that expects the Repository[T] interface:

func Foo[R Repository[R]](repo R) {
  // ...
}

func main() {
  repository := &repository{}
  Foo(repository)
}

However, the generated mocks generate a struct that follows the interface (as expected):

type MockRepository[T interface{}] struct {
  mock.Mock
}

func (_m *MockRepository[T]) Clone() T {
  // ...
}

The problem is that these generated structs now cannot be used in the Foo function because their clone methods cannot infer T.

For an example with a little bit more added, please see this repository: https://github.com/DustinJSilk/mockery-cloneable/tree/main

Mockery Version

v2.39.2

Golang Version

1.21.6

Installation Method

Steps to Reproduce

  1. Clone the repository
  2. See the errors in your IDE

Expected Behavior

There is no way to have an interface that clones itself without using generics. It would help if there was a way to tell Mockery to replace the generics with a specific type, in this case, itself.

Actual Behavior

Is correct, but also doesnt work. It generates the mocks usings the interface definition but it needs more flexibility to work correctly.

LandonTClipp commented 8 months ago

@DustinJSilk

It took me a minute to wrap my head around the question but I think I understand. This is basically a self-referential problem coming from the fact that .Clone() is returning the same generic type that was used when instantiating the mocks.MockClient type. This loop is generated when providing concrete types to the generic constraints:

mockRepository :=mocks.NewMockRepository[*testing.T, *mocks.MockClient[*mocks.MockClient[*mocks.MockClient[etc. forever and ever]]](t)

I forked your example and manually modified the mocks to provide a completely working example of what we would ultimately want mockery to generate: https://github.com/LandonTClipp/mockery-cloneable/commit/d18f1dd7c14fbde7195a17f58b217d8dff9753b7

The proposal is to add new config parameters that allow mockery to replace the generic constraints with concrete types. The changes I made to the mock implementation should be what mockery would generate.

My questions for you:

  1. Does my fork of your reproducer seem to actually fix your problem?
  2. Would you be willing to submit a PR for this, assuming we both agree on the solution I proposed?
DustinJSilk commented 8 months ago

Hi @LandonTClipp ,

Thanks for taking the time to look into this!

1) Yes it does, I ended up manually editing the mocks in the same way in my project and it works as expected. 2) I'm happy to have a crack at it over the next few days when i get a moment

LandonTClipp commented 8 months ago

Okay great. It would be much appreciated if you could try for an implementation here. Otherwise, I can probably do it myself sometime in the next few weeks. Although, this is such a rare situation that it might get thrown behind some other high priority tasks.