Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Mocking dependencies in Go

Sebastian Dahlgren
Agrea
Published in
4 min readJul 5, 2017

Overview

We all use a lot of packages when constructing our applications, both internally developed and third party packages. This blog post will show how you can easily mock these packages using mockery and stretchr/testify/mock.

Let’s dive into it; An example

When writing unit tests for our code we might find the external packages problematic. Consider this case:

import "github.com/mediocregopher/radix.v2/redis"type Handler struct {
db *redis.Client
}
func (h *Handler) Ping() (string, error) {
res := h.db.Cmd("INCR", "ping:count")
if res.Err != nil {
return "", res.Err
}
return "pong", nil
}

There is a Handler which is holding the Redis client. Then there is a Ping method on the Handler which is using the Redis client. Now, when testing the Ping method we’d like to check a couple of things:

  • When the database is and is not throwing an error
  • The response text in positive and negative cases

This would require us to have an actual Redis database running to make the Cmd call. However for the purpose of testing Ping we’re less interested in testing the actual database. Our concern is the Ping method itself and how it behaves under certain circumstances (some of which maybe very hard to reproduce).

The way out; Creating an interface

The observant reader noticed that the db is a *redis.Client. Which means that it’s a pointer to a struct. Mocking structs is hard, since we cannot replace them with our own implementation. So let’s refactor our code to use an interface instead.

import "github.com/mediocregopher/radix.v2/redis"type storager interface {
Cmd(string, ...interface{}) *redis.Resp
}
type Handler struct {
db storager
}
func (h *Handler) Ping() (string, error) {
res := h.db.Cmd("INCR", "ping:count")
if res.Err != nil {
return "", res.Err
}
return "pong", nil
}

We have now created a small interface called storager which defines the behavior — what methods should be present and what they return — of the database.

Let’s next look at how to test this:

import (
"errors"
"testing"
"github.com/mediocregopher/radix.v2/redis"
"github.com/stretchr/testify/assert"
)
func TestPing(t *testing.T) {
sampleErr := errors.New("sample error")
tests := map[string]struct {
storageErr error
response string
err error
}{
"successful": {
storageErr: nil,
response: "pong",
err: nil,
},
"with db error": {
storageErr: sampleErr,
response: "",
err: sampleErr,
},
}
for name, test := range tests {
t.Logf("Running test case: %s", name)
storage := &mockStorager{}
storage.
On("Cmd", "INCR", []interface{}{"ping:count"}).
Return(&redis.Resp{
Err: test.storageErr,
}).
Once()
h := &Handler{
db: storage,
}
response, err := h.Ping()
assert.Equal(t, test.err, err)
assert.Equal(t, test.response, response)
storage.AssertExpectations(t)
}
}

Let’s look at this step by step.

  1. First I’m setting up two test cases in my tabled based test. One for testing when the database is not returning an error. And one for when it does.
  2. I define storage which is of the type *mockStorager. This is important. I’ll get to why soon. Notice also how I’m setting input parameters and the return value for the Cmd.
  3. Then I validate the response with the test definition.
  4. storage.AssertExpectations(t) asserts that the expectations fined on the store has been full filled. In this case that would be that the Cmd("INCR", "ping:count") was called once and only once.

Now, the *mockStorager is a struct I have generated using mockery (mockery is a tool for autogenerating mock, if you aren’t using it; please read up on it!). It returns a ready to use mock based on any given interface. The following command was used in this case:

mockery -name storager -inpkg .

Now I’m able to make mocked tests of the Ping method, without having to write a mocked database on my own.

The generated mocked interface looks like below. The code is using testify/mock to implement the actual mock. mockery is only used to generate the code.

import mock "github.com/stretchr/testify/mock"
import redis "github.com/mediocregopher/radix.v2/redis"
// mockStorager is an autogenerated mock type for the storager type
type mockStorager struct {
mock.Mock
}
// Cmd provides a mock function with given fields: _a0, _a1
func (_m *mockStorager) Cmd(_a0 string, _a1 ...interface{}) *redis.Resp {
ret := _m.Called(_a0, _a1)
var r0 *redis.Resp
if rf, ok := ret.Get(0).(func(string, ...interface{}) *redis.Resp); ok {
r0 = rf(_a0, _a1...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*redis.Resp)
}
}
return r0
}
var _ storager = (*mockStorager)(nil)

More on testify/mock

There are a number of cool things you can do with this mock object. In the above test we only used the mock.On(...).Return(...).Once(). But you could also define how many times you’d expect a certain call to be made.

More details about stretchr/testify/mock can be found in the package documentation.

Conclusion

There are two main takeaways from this blog post that I would like to highlight:

  1. Use interfaces to inject your dependencies. That way testing will become much smoother.
  2. Take help from great community provided software like mockery and stretchr/testify/mock to ease the burden and stay focused on your core business.

--

--

Agrea
Agrea

Published in Agrea

△grea is a Go coding studio. We offer Go consultants world wide remotely and in person in Europe and India (at the moment).

Sebastian Dahlgren
Sebastian Dahlgren

Written by Sebastian Dahlgren

Developer and Go ❤. Love code. And clouds. Nerd on a mission.

Responses (4)