Mocking interfaces with typed functions in Go
Tags: go, mocks, testing
Published: 2020-12-05 10:22:14

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`](https://pkg.go.dev/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.

```go
// 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.

```go
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.

```go
// 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.

```go
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.

```go
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`.

```go
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](https://en.wikipedia.org/wiki/Single-responsibility_principle) and a design improvement opportunity!


```go
// 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.
{% spoiler Example.  %}

```go
// 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
	})
}

```

{% endspoiler %}