DEV Community

loading...

Mocking interfaces with typed functions in Go

vearutop profile image Viacheslav Poturaev ・Updated on ・4 min read

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

Discussion

pic
Editor guide