DEV Community

Viacheslav Poturaev
Viacheslav Poturaev

Posted on • Edited on

Mocking interfaces with typed functions in Go

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)
}
Enter fullscreen mode Exit fullscreen mode

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
    })))
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
        },
    }))
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
        },
    }))
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
    })
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)