Command make.go.mock generates type-safe mocks for Go interfaces and functions.
It looks like this:
repo, assertMock := (&KeyValuesRepositoryMocker{}).Describe().
Get().Takes("foo").Returns(42, nil).Times(1).
Put().Takes("foo").And(43).Returns(nil).Times(1).
Mock()
defer assertMock(t)
err := IncreaseCounter(repo, "foo")
Some core principles behind make.go.mock are:
- Mock code should be maintainable. It doesn't use
interface{}
. It doesn't reflect. As hand-written Go code would, it leverages Go's typechecker to get compile time errors when things don't match. It creates clear stack traces when things go wrong, with no indirection or complex helper functions in the middle. - Mock code should be readable. The generated API is kind of verbose, but reads natural, has clear names and well-documented components.
- Be flexible. Basic "any" and "equal" matchers are provided, but you can also provide your custom matchers. Or just pass the
-bare
flag to get a bare minimum mock and do everything yourself.
go get github.com/tcard/make.go.mock
make.go.mock takes a type name (interface or function) as argument and generates Go code to mock it.
It's intended to be used with go generate
.
For a full list of flags:
make.go.mock -h
See examples/examples.go for actual examples of go:generate
directives.
Check out also a full example in the docs, or the generated API for the examples package.
make.go.mock leverages github.com/google/go-cmp to provide rich error feedback on unexpected values. It also tries to provide enough context information to help track down mistakes easily.
For instance, look at this code:
m := map[string]map[MyStruct]bool{}
expectedErr := errors.New("expected")
mock, assertMock := (&MyInterfaceMocker{}).Describe().
ShouldBeFun().Takes(123).And(m).AndAny().Returns(1, expectedErr).
ShouldBeFun().Takes(456).And(m).AndAny().Returns(2, errors.New("err 2")).
Mock()
defer assertMock(t)
_, _ = mock.ShouldBeFun(789, m)
The call mock.ShouldBeFun(789, m)
doesn't match either of the described expectations, so the mock doesn't know what to return. It then panics like this:
panic: no matching candidate for call to mock for MyInterface.ShouldBeFun with args:
789
map[string]map[examples.MyStruct]bool{}
[]chan<- <-chan struct {}(nil)
failing candidates:
candidate described at /path/to/make.go.mock/examples/mocks_test.go:28:
parameter #1 mismatch:
int(
- 123,
+ 789,
)
candidate described at /path/to/make.go.mock/examples/mocks_test.go:29:
parameter #1 mismatch:
int(
- 456,
+ 789,
)
For this method:
type Index interface {
Put(key string, value interface{}) (bool, error)
}
mockgen generates this:
func (mr *MockIndexMockRecorder) Put(arg0, arg1 interface{}) *gomock.Call
The string arguments are turned into interface{}
, losing type safety in the process. So if you change the type of the arguments, the test code will still compile, only to crash when run. It's even more fragile if the return types or number of arguments change.
Compare this with the code generated by make.go.mock:
func (d IndexMockDescriptor) GetTwo() *IndexGetTwoMockDescriptor
func (d *IndexPutMockDescriptor) Takes(key string, opts ...cmp.Option) IndexPutMockDescriptorWith1Arg
func (d *IndexPutMockDescriptor) TakesAny() IndexPutMockDescriptorWith1Arg
func (d *IndexPutMockDescriptor) TakesMatching(match func(key string) error) IndexPutMockDescriptorWith1Arg
func (d IndexPutMockDescriptorWith1Arg) And(value interface{}, opts ...cmp.Option) IndexPutMockDescriptorWith2Args
func (d IndexPutMockDescriptorWith1Arg) AndAny() IndexPutMockDescriptorWith2Args
func (d IndexPutMockDescriptorWith1Arg) AndMatching(match func(value interface{}) error) IndexPutMockDescriptorWith2Args
The original types are kept, and type information is preserved.
mockgen doesn't support mocking function types, while make.go.mock does.
mockery generates mocks that leverage github.com/stretchr/testify/mock, which isn't a mock generator, but just a runtime library.
mockery only does part of the job that a mock generator could do, though: it doesn't generate the code to describe what the mock does. For that, you're supposed to use plain testify/mock.
This means that type-safety is completely lost. To describe mocks, you use methods like these:
func (m *Mock) On(methodName string, arguments ...interface{}) *Call
func (c *Call) Return(returnArguments ...interface{}) *Call
If the method name, number and/or type of arguments and/or return values change, you'll only notice your mocks are broken when you run the tests.
mockery also doesn't support mocking function types, while make.go.mock does.
Now let's see how you would use each:
mockgen
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockIndex := mock_user.NewMockIndex(ctrl)
mockIndex.EXPECT().Put("a", 1).Return(true, nil)
mockIndex.EXPECT().Put("b", gomock.Any()).Return(false, errors.New("broken"))
// Use mock
_, _ = mockIndex.Put("a", 1)
mockery
mockIndex := &MockIndex{}
defer mockIndex.AssertExpectations()
mockIndex.On("Put", "a", 1).Returns(true, nil)
mockIndex.On("Put", "b", mock.Anything).Returns(false, errors.New("broken"))
// Use mock
_, _ = mockIndex.Put("a", 1)
make.go.mock
mockIndex, assertMock := (&IndexMocker{}).Describe().
Put().Takes("a").And(1).Returns(true, nil).AtLeastTimes(1).
Put().Takes("b").AndAny().Returns(false, errors.New("broken")).
Mock()
defer assertMock(t)
// Use mock
_, _ = mockIndex.Put("a", 1)
make.go.mock is more verbose; it's the tradeoff it makes to keep type safety by generating several methods per argument. Otherwise, by cramming all use cases in a single method, you force the method to take interface{}
s in order to express things like "any value".