mockcompose
was originally built to address a Go anti-pattern use case scenario. To be exact, the use case can be described with following Java example:
in Java, we can mix real method call with mocked sibling method calls like this:
FooService fooService = PowerMockito.mock(FooService.class);
PowerMockito.doCallRealMethod().when(fooService).SomeFooMethod());
In the example, SomeFooMethod() will be called to run real implementation code, while any sibling method that SomeFooMethod() calls will be taken from the mocked version. This ability can give us fine-grained control in unit testing, in world of Object Oriented languages.
Go is a first-class function programming language, Go best practices prefer small interfaces, in the extreme side of the spectrum, per-function interface would eliminate the needs of such usage pattern to be supported at all in mocking. This might be the reason why most Go mocking tools support only interface mocking.
Nevertheless, if you ever come to here, you may be struggling in balancing the ideal world and practical world, try mockcompose
to solve your immediate needs and you are recommended to follow Go best practices to refactor your code later, to avoid Go anti-pattern as mentioned above if possible.
mockcompose
also supports generating mockery compatible code for Go interfaces and regular functions, which could help pave the way for your code to evolve into ideal shape.
Note: Go class here refers to Go struct
with functions that take receiver objects of the struct
type.
go install github.com/kelveny/mockcompose
mockcompose generates mocking implementation for Go classes, interfaces and functions.
-c string
name of the source class to generate against
-help
if set, print usage information
-i string
name of the source interface to generate against
-mock value
name of the function to be mocked
-n string
name of the generated class
-p string
path of the source package in which to search interfaces and functions
-pkg string
name of the package that the generated class resides
-real value
name of the method function to be cloned from source class or source function
-testonly
if set, append _test to generated file name (default true)
-v if set, print verbose logging messages
-version
if set, print version information
-pkg
option is usually omitted, mockcompose
will derive Go package name automatically from current working directory.
You can use multiple -real
and -mock
options to specify a set of real class method functions to clone and another set of class method functions to mock.
mockcompose
is recommended to be used in go generate
:
//go:generate mockcompose -n testFoo -c foo -real Foo -mock Bar
In the example, mockcompose
will generate a testFoo class with Foo() method function be cloned from real foo class implementation, and Bar() method function be mocked.
source Go class code: foo.go
package foo
type Foo interface {
Foo() string
Bar() bool
}
type foo struct {
name string
}
var _ Foo = (*foo)(nil)
func (f *foo) Foo() string {
if f.Bar() {
return "Overriden with Bar"
}
return f.name
}
func (f *foo) Bar() bool {
if f.name == "bar" {
return true
}
return false
}
go generate
configuration: mocks.go
//go:generate mockcompose -n testFoo -c foo -real Foo -mock Bar
//go:generate mockcompose -n FooMock -i Foo
package foo
mockcompose
generated code: mockc_testFoo_test.go
// CODE GENERATED AUTOMATICALLY WITH github.com/kelveny/mockcompose
// THIS FILE SHOULD NOT BE EDITED BY HAND
package foo
import (
"github.com/stretchr/testify/mock"
)
type testFoo struct {
foo
mock.Mock
}
func (f *testFoo) Foo() string {
if f.Bar() {
return "Overriden with Bar"
}
return f.name
}
func (m *testFoo) Bar() bool {
_mc_ret := m.Called()
var _r0 bool
if _rfn, ok := _mc_ret.Get(0).(func() bool); ok {
_r0 = _rfn()
} else {
if _mc_ret.Get(0) != nil {
_r0 = _mc_ret.Get(0).(bool)
}
}
return _r0
}
You can now write unit tests to test at fine-grained granularity. This can enable to test individual or a group of class method functions, with dependency closure be mocked.
func TestFoo(t *testing.T) {
assert := require.New(t)
fooObj := &testFoo{}
// Mock sibling method Bar()
fooObj.On("Bar").Return(false)
s := fooObj.Foo()
assert.True(s == "")
}
1. My class method not only has callouts to sibling methods, but also callouts to functions imported from other packages, and I want to mock these imported functions, how can I do that?
Answer: Check out mockcompose
self-test example mockfn
go generate
configuration: mocks.go
//go:generate mockcompose -n mockFmt -p fmt -mock Sprintf
//go:generate mockcompose -n mockJson -p encoding/json -mock Marshal
//go:generate mockcompose -n mockSampleClz -c sampleClz -real "methodThatUsesGlobalFunction,fmt=fmtMock"
//go:generate mockcompose -n mockSampleClz2 -c sampleClz -real "methodThatUsesMultileGlobalFunctions,fmt=fmtMock:json=jsonMock"
//go:generate mockcompose -n mockSampleClz3 -c sampleClz -real "methodThatUsesMultileGlobalFunctions,fmt=fmtMock"
package mockfn
With this configuration, mockcompose
generates Go classes for package fmt
and encoding/json
, the generated Go classes are equipped with mocked function implementation. mockcompose
also clones the subject class method with local overrides, thus enables callouts to be redirected to mocked implementation.
fn_test.go
package mockfn
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
var jsonMock *mockJson = &mockJson{}
var fmtMock *mockFmt = &mockFmt{}
func TestSampleClz(t *testing.T) {
assert := require.New(t)
// setup function mocks
jsonMock.On("Marshal", mock.Anything).Return(([]byte)("mocked Marshal"), nil)
fmtMock.On("Sprintf", mock.Anything, mock.Anything).Return("mocked Sprintf")
// inside mockSampleClz.methodThatUsesMultileGlobalFunctions: fmt.Sprintf is mocked
sc := mockSampleClz{}
assert.True(sc.methodThatUsesGlobalFunction("format", "value") == "mocked Sprintf")
// inside mockSampleClz2.methodThatUsesMultileGlobalFunctions: both json.Marshal()
// and fmt.Sprintf are mocked
sc2 := mockSampleClz2{}
assert.True(sc2.methodThatUsesMultileGlobalFunctions("format", "value") == "mocked Marshalmocked Sprintf")
// inside mockSampleClz3.methodThatUsesMultileGlobalFunctions: json.Marshal() is not mocked,
// fmt.Sprintf is mocked
sc3 := mockSampleClz3{}
assert.True(sc3.methodThatUsesMultileGlobalFunctions("format", "value") == "\"format\"mocked Sprintf")
}
2. I want to test my function with callouts to functions imported from other packages, and I want to mock these imported functions, how can I do that?
Answer: Check out mockcompose
self-test example clonefn
go generate
configuration: mocks.go
//go:generate mockcompose -n mockFmt -p fmt -mock Sprintf
//go:generate mockcompose -n mockJson -p encoding/json -mock Marshal
//go:generate mockcompose -n clonedFuncs -real "functionThatUsesMultileGlobalFunctions,fmt=fmtMock:json=jsonMock" -real "functionThatUsesGlobalFunction,fmt=fmtMock" -real "functionThatUsesMultileGlobalFunctions2,fmt=fmtMock"
package clonefn
With this configuration, mockcompose
generates Go classes for package fmt
and encoding/json
, the generated Go classes are equipped with mocked function implementation. mockcompose
also clones the subject function with local overrides, thus enables callouts to be redirected to mocked implementation.
fn_test.go
package clonefn
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
var jsonMock *mockJson = &mockJson{}
var fmtMock *mockFmt = &mockFmt{}
func TestClonedFuncs(t *testing.T) {
assert := require.New(t)
// setup function mocks
jsonMock.On("Marshal", mock.Anything).Return(([]byte)("mocked Marshal"), nil)
fmtMock.On("Sprintf", mock.Anything, mock.Anything).Return("mocked Sprintf")
// inside functionThatUsesMultileGlobalFunctions: fmt.Sprintf is mocked
assert.True(functionThatUsesGlobalFunction_clone("format", "value") == "mocked Sprintf")
// inside functionThatUsesMultileGlobalFunctions: both json.Marshal()
// and fmt.Sprintf are mocked
assert.True(functionThatUsesMultileGlobalFunctions_clone("format", "value") == "mocked Marshalmocked Sprintf")
// inside functionThatUsesMultileGlobalFunctions2: json.Marshal() is not mocked,
// fmt.Sprintf is mocked
assert.True(functionThatUsesMultileGlobalFunctions2_clone("format", "value") == "\"format\"mocked Sprintf")
}
Answer: Check out mockcompose
self-test example mockintf
go generate
configuration: mocks.go
//go:generate mockcompose -n MockSampleInterface -i SampleInterface
//go:generate mockcompose -n mockFoo -i Foo -p github.com/kelveny/mockcompose/test/foo
package mockintf
With this configuration, mockcompose
generates mocked interface implementation both for an interface defined in its own package and an interface defined in other package.
intf_test.go
package mockintf
import (
"testing"
"github.com/kelveny/mockcompose/test/foo"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestMockVariadic(t *testing.T) {
assert := require.New(t)
m := MockSampleInterface{}
m.On("Variadic",
"string1: %s, string2: %s, string3: %s",
"value1", "value2", "value3",
).Return("success")
assert.True(m.Variadic(
"string1: %s, string2: %s, string3: %s",
"value1", "value2", "value3",
) == "success")
}
...
Answer: Yes. mockcompose
can group a set of functions into a generated Go class, the generated Go class has embedded mock object through which function behavior can be mocked.
go generate
configuration: mocks.go
//go:generate mockcompose -n mockFmt -p fmt -mock Sprintf
//go:generate mockcompose -n mockJson -p encoding/json -mock Marshal
With this configuration, mockcompose
can generate mocking Go class mockFmt
and mockJson
that implement Sprintf
and Marshal
respectively. Callers of these functions can then use method/function local overrides to connect callouts of method/function to these generated Go classes.
These techniques have been used in examples of the questions above.
Answer: Check out mockcompose
self-test example yaml
go generate
configuration: mocks.go
//go:generate mockcompose
package yaml
go generate
YAML configuration file: .mockcompose.yaml
mockcompose:
- name: mockFmt
testOnly: true
sourcePkg: fmt
mock:
- Sprintf
- name: mockJson
testOnly: true
sourcePkg: encoding/json
mock:
- Marshal
- name: mockSampleClz
testOnly: true
className: sampleClz
real:
- "methodThatUsesGlobalFunction,fmt=fmtMock"
- name: mockSampleClz2
testOnly: true
className: sampleClz
real:
- "methodThatUsesMultileGlobalFunctions,fmt=fmtMock:json=jsonMock"
- name: mockSampleClz3
testOnly: true
className: sampleClz
real:
- "methodThatUsesMultileGlobalFunctions,fmt=fmtMock"
- name: MockSampleInterface
testOnly: true
interfaceName: SampleInterface
- name: mockFoo
testOnly: true
interfaceName: Foo
sourcePkg: github.com/kelveny/mockcompose/test/foo
- name: mockFmtclonedFuncs
testOnly: true
real:
- "functionThatUsesMultileGlobalFunctions,fmt=fmtMock:json=jsonMock"
- "functionThatUsesGlobalFunction,fmt=fmtMock"
- "functionThatUsesMultileGlobalFunctions2,fmt=fmtMock"
If mockcompose
detects .mockcompose.yaml
or .mockcompose.yml
in package directory, it will load code generation configuration from the file.