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

Add mock generation with expecter #396

Merged
merged 2 commits into from Jan 25, 2022

Conversation

Gevrai
Copy link

@Gevrai Gevrai commented Jul 13, 2021

Note: This PR is still prototypical, code is a bit messy. However, the general functionality should be stable and I'm mostly requesting for comments to see if that is something anyone would be interested on the main branch (currently using it in other projects).

All comments are more than welcomed.

Motivation for feature

Setting up mocks has always been a lot of guesswork or going back and forth between the tests and the interface's definition. The mockery framework already extracts all necessary information for this, it seems a logical next step to have type explicit mock construction.

Goal

Adds flag --with-expecter which generates boilerplate code over all of relevant mocks's functions that are missing type safety with the current way of defining mocks. Generates the usual mock boilerplate, plus an Expecter structure for setting up calls. The naming is inspired by GoMock.

Works with variadic methods, but not if the flag --unroll-variadic=false is set (not using it in my projects, didn't take the time to look at feasibility with it really...)

Example

Given an interface such as

type Requester interface {
	Get(path string) (string, error)
}

Generates the following as well as the usual Get mock.Called wrapper

// Requester is an autogenerated mock type for the Requester type
type Requester struct {
        mock.Mock
}

type Requester_Expecter struct {
	mock *mock.Mock
}

func (_m *Requester) EXPECT() *Requester_Expecter {
	return &Requester_Expecter{mock: &_m.Mock}
}

// Requester_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
type Requester_Get_Call struct {
	*mock.Call
}

// Get is a helper method to define mock.On call
//  - path string
func (_e *Requester_Expecter) Get(path interface{}) *Requester_Get_Call {
	return &Requester_Get_Call{Call: _e.mock.On("Get", path)}
}

func (_c *Requester_Get_Call) Run(run func(path string)) *Requester_Get_Call {
	_c.Call.Run(func(args mock.Arguments) {
		run(args[0].(string))
	})
	return _c
}

func (_c *Requester_Get_Call) Return(_a0 string, _a1 error) *Requester_Get_Call {
	_c.Call.Return(_a0, _a1)
	return _c
}

So that one can use it like so

requesterMock := Requester{}
requesterMock.EXPECT().Get("some path").Return("result", nil)
requesterMock.EXPECT().
	Get(mock.Anything).
	Run(func(path string) { fmt.Println(path, "was called") }).
	// Can still use return functions by getting the embedded mock.Call
	Call.Return(func(path string) string { return "result for " + path }, nil)

Pros:

  • Backward compatible with current mockery
  • Still keeps advantages of mock.Anything and mock.MatchedBy(func), but prints the concrete type in docstring of the Expecter method.
  • IntelliSense, autocomplete, call documentation... when setting up mocked calls.
  • Compile time error on erroneous arguments.
  • Easy refactoring when interface changes.

Cons:

  • Does not work if the mocked interface already has a method named EXPECT (should be easy to fix, unnecessary for RFC)
  • About 2.5x more boilerplate code.
  • Uses golang's templating system which is non standard for this project.
  • Not all mock.Call methods are defined, if one wants to use Run or Return, it needs to be before the others, which might be a bit confusing. For example EXPECT().Get("").Return(...).Once().Run(...) wouldn't compile while EXPECT().Get("").Return(...).Run(...).Once() would. This could be fixed by redefining all underlying methods to return the concrete Call struct, at the cost of more boilerplate of course.

Further amelioration

The *_Expecter and *_Call structs could be unexported and still usable as far as I can see.

Addition of a RunAndReturn function that takes a function with exact same signature as mocked method and defines the Return function with the result of the Run function. I ran into a potential race issue with this though and didn't go further. Might look into it more if there is a need.

Make it work with --unroll-variadic maybe, for now it crashes. To be honest, I think that might be useless or even counter-productive though...

@Gevrai Gevrai marked this pull request as ready for review July 13, 2021 13:59
@k1ng440
Copy link

k1ng440 commented Jul 16, 2021

great job. works great.

@k1ng440
Copy link

k1ng440 commented Jul 16, 2021

@jimmidyson Please review this.

@Gevrai
Copy link
Author

Gevrai commented Jul 16, 2021

@k1ng440 thanks!

@Gevrai
Copy link
Author

Gevrai commented Aug 11, 2021

@LandonTClipp Whenever you have time, can you give this a quick look?

@LandonTClipp
Copy link
Contributor

This is really f-ing cool, I'm really excited to see something like this.

My first intuition is that this should be part of a new major version bump. I've been considering cutting over to an alpha v3 soon and I think I'd like to have the expecter schema to be the default behavior, and not rely on backwards compatibility. It would be really cool if the On (etc) methods were always type safe.

One thing I was initially worried about was the fact that we are overriding methods in testify, and my concern was that we are now forced to maintain equivalence between mockery's generated functions and testify's. But I realized that you could just call the promoted field to "exit" the expecter interface and revert back to testify's call flow.

I am going to need some more time to look this over and come up with a battle plan, but this has my attention. Really stellar work!

@Gevrai
Copy link
Author

Gevrai commented Sep 16, 2021

Thanks a lot! Ping me if you have any questions regarding this feature or you need me to add/change anything. I'll take some time during the weekend to rebase the branch following the recent pull requests.

@LandonTClipp
Copy link
Contributor

After spending the last half year (!) giving it thought I finally think this is good to go through. I won't be introducing a major version bump, this will just be incorporated in v2.

@LandonTClipp LandonTClipp merged commit 66d6564 into vektra:master Jan 25, 2022
@johnrichardrinehart
Copy link

@LandonTClipp @Gevrai Why aren't the Get methods strongly typed? It seems like it'd be beneficial/consistent if the signature changed from

func (_e *Requester_Expecter) Get(path interface{}) *Requester_Get_Call

to

func (_e *Requester_Expecter) Get(path string) *Requester_Get_Call

for this example.

@Gevrai
Copy link
Author

Gevrai commented Feb 11, 2022

@johnrichardrinehart It is to still be able to use testify.Mock's functionalities for matching, such as mock.Anything, mock.AnythingOfType("someType") and mock.MatchBy(...)
I use those a lot in my mocking, so implemented it accepting them.

Exact expected types are given in Expecter method's docstring.

It is maybe not ideal, but I don't see a way around it, unless we get generics type on method some day (not in 1.18 even) with some changes needed on testify itself...

func (_e *Requester_Expecter) Get[T type{mock.Anything|mock.AnythingOfType|mock.MatchBy|string}](path T) *Requester_Get_Call

Open to ideas though.

@johnrichardrinehart
Copy link

johnrichardrinehart commented Feb 11, 2022

It is to still be able to use testify.Mock's functionalities for matching, such as mock.Anything, mock.AnythingOfType("someType") and mock.MatchBy(...)
I use those a lot in my mocking, so implemented it accepting them.

Ahhh, of course. I'm only starting to use mockery and my first few tests have all been with exact matches. This totally makes sense.

Re generics: Yeah, unfortunately, go's generics implementation is too weak to benefit us, here. I can't offer any suggestion outside of extending the generation to produce a

func (_e *Requester_Expecter) GetTyped(path string) *Requester_Get_Call

which would wrap the Get that you already implemented.

@Gevrai would you mind pointing me to a few good public use cases where you used this this expecter pattern (or just mockery, generally)? I'm trying to find some good source examples ("in the wild") to guide me.

@Gevrai
Copy link
Author

Gevrai commented Feb 11, 2022

@Gevrai would you mind pointing me to a few good public use cases where you used this this expecter pattern (or just mockery, generally)? I'm trying to find some good source examples ("in the wild") to guide me.

This feature was added very recently, I don't think it is really used in any public repos (mine isn't public sadly).

Maybe the tests might be of help a bit? https://github.com/vektra/mockery/blob/master/pkg/fixtures/mocks/expecter_test.go

That being said, it is expected to be used the exact same way as you would use the old m.On("Somefunction",...) methods, but called as m.EXPECT().Somefunction(...) instead.

I even used a simple search and replace to transform all my mocked calls in tests without issues.

@johnrichardrinehart
Copy link

I didn't think to look at the tests 😆 . Smart! Alright, I think that's enough for me. Thanks @Gevrai ! Great work!

@LandonTClipp
Copy link
Contributor

LandonTClipp commented Apr 18, 2022

@Gevrai in #406 @grongor brings up a good point.

#406 (comment)

If you had some time could you fix the location of these mocks: https://github.com/vektra/mockery/tree/master/pkg/fixtures/mocks

Apologies for not catching this in the original PR. Thanks again for the great work!

@Gevrai
Copy link
Author

Gevrai commented Apr 19, 2022

TBH I did not expect this feature to be merged without more comment, code was a bit messy in some places 😅

I will try to find some time this evening to fix this and create a PR.

@subtle-byte
Copy link

@johnrichardrinehart I've created mock generator where methods are strongly typed) https://github.com/subtle-byte/mockigo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants