Viacheslav Poturaev
Posted on December 5, 2020
When it comes to mocking external dependencies in Go unit tests, one of the most popular approaches is to leverage github.com/golang/mock/gomock
with code generation.
Although this approach serves the purpose, it has some downsides:
- maintenance cost to (re)generate mocks,
- reduced compile-time type safety due to
interface{}
arguments and variadic returns, - poor IDE assistance because of reduced type safety,
- verbose usage syntax due to declarative model,
- potential negative impact on project code coverage for a large body of unused/untested generated code.
When dealing with large interfaces gomock
provide enough convenience to justify the downsides. However for smaller interfaces hand-written mocks might be a better fit.
Interface with single method can be implemented on top of a typed function.
// SomeDoer does something.
type SomeDoer interface {
DoSomething(a string, b int) (bool, error)
}
// SomeDoerFunc implements SomeDoer.
type SomeDoerFunc func(a string, b int) (bool, error)
// DoSomething does whatever is necessary.
func (f SomeDoerFunc) DoSomething(a string, b int) (bool, error) {
return f(a, b)
}
Then, test usage is a regular non-magical Go code transparent for IDE static analysis and with static type safety. Expectations of arguments and returned results are explicit and under full control of a developer.
func TestSomething(t *testing.T) {
// testableDependent represents some piece of code with a dependency on SomeDoer to test.
testableDependent := func(sd SomeDoer) bool {
a := "abc"
b := 123
ok, err := sd.DoSomething(a, b)
return ok && err == nil
}
// Test cases.
assert.True(t, testableDependent(SomeDoerFunc(func(a string, b int) (bool, error) {
// Assert arguments if necessary.
assert.Equal(t, a, "abc")
assert.Equal(t, b, 123)
return true, nil
})))
assert.False(t, testableDependent(SomeDoerFunc(func(a string, b int) (bool, error) {
return true, errors.New("failed")
})))
assert.False(t, testableDependent(SomeDoerFunc(func(a string, b int) (bool, error) {
return false, nil
})))
}
While such kind of mocking is very simple for single-function interfaces, things get more complicated for richer interfaces. Every function in the interface would need a functional mock.
// SomeRepository has strings.
type SomeRepository interface {
Find(id int) (string, bool)
Add(id int, value string)
}
// SomeRepositoryFindFunc implements a part of SomeRepository.
type SomeRepositoryFindFunc func(id int) (string, bool)
// Find delegates finding.
func (f SomeRepositoryFindFunc) Find(id int) (string, bool) {
return f(id)
}
// SomeRepositoryAddFunc implements a part of SomeRepository.
type SomeRepositoryAddFunc func(id int, value string)
// Add delegates adding.
func (f SomeRepositoryAddFunc) Add(id int, value string) {
f(id, value)
}
And then we can use a struct
with embedded fields to "build" an instance of interface for the test.
func TestWithRepository(t *testing.T) {
// testableDependent represents some piece of code with a dependency on SomeRepository to test.
testableDependent := func(sd SomeRepository) bool {
sd.Add(123, "abc")
s, found := sd.Find(123)
return found && s == "abc"
}
assert.True(t, testableDependent(struct {
SomeRepositoryAddFunc
SomeRepositoryFindFunc
}{
func(id int, value string) {
assert.Equal(t, 123, id)
assert.Equal(t, "abc", value)
},
func(id int) (string, bool) {
return "abc", true
},
}))
}
To mock large interfaces with only partial actual usage you can use a proxy mock tailored for a particular scenario.
Also you may consider to refactor the code to depend on a reduced interface (that is actually in use) in the first place.
type proxyMock struct {
// SomeRepository enables interface compatibility.
SomeRepository
f SomeRepositoryFindFunc
}
// Find overrides embedded SomeRepository to dispatch into a provided function.
func (m proxyMock) Find(id int) (string, bool) {
return m.f(id)
}
Test execution must not invoke unmocked methods of the dependency or it will panic with runtime error: invalid memory address or nil pointer dereference
.
func TestWithPartialDependency(t *testing.T) {
// testableDependent represents some piece of code with a dependency on SomeRepository to test.
testableDependent := func(sd SomeRepository) bool {
// sd.Add(123, "abc") // This statement would panic since SomeRepository is nil.
s, found := sd.Find(123)
return found && s == "abc"
}
assert.True(t, testableDependent(proxyMock{
f: func(id int) (string, bool) {
return "abc", true
},
}))
}
Small interfaces are not only convenient for mocking, but also allow a more granular dependencies management which is a good thing.
The great thing about large interfaces is that they may indicate a violation of Single Responsibility Principle and a design improvement opportunity!
// SomeFinder finds strings.
type SomeFinder interface {
Find(id int) (string, bool)
}
// SomeAdder stores strings.
type SomeAdder interface {
Add(id int, value string)
}
Splitting an interface allows nice things like independent decoration.
Example.
// Repo pretends to store strings.
type Repo struct{}
func (r *Repo) Add(id int, value string) { panic("implement me") }
func (r *Repo) Find(id int) (string, bool) { panic("implement me") }
// These functions pretend to setup actual dependents.
func setupFindHandler(f SomeFinder) { panic("implement me") }
func setupAddHandler(f SomeAdder) { panic("implement me") }
// Initializing resources with independent decoration.
func setup() {
repo := &Repo{}
// Decorating finder with caching.
setupFindHandler(cacheSomeFinder(repo))
// Decorating adder with logging.
setupAddHandler(SomeRepositoryAddFunc(func(id int, value string) {
log.Println("Adding string", id, value)
repo.Add(id, value)
}))
}
// cacheSomeFinder wraps finder and caches its results.
func cacheSomeFinder(upstream SomeFinder) SomeFinder {
var cache sync.Map
return SomeRepositoryFindFunc(func(id int) (string, bool) {
v, _ := cache.Load(id)
if s, ok := v.(string); ok {
return s, true
}
s, ok := upstream.Find(id)
if ok {
cache.Store(id, s)
}
return s, ok
})
}
Posted on December 5, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.