how-to-mock

How to mock behaviors in Go

Welcome! If you’re getting to work with Go and you’re coming from a high-level language like Java or Python then I’m pretty sure you’re going to struggle a little bit when trying to test the code you write in Go.

One important thing that you need to have in mind is that the whole idea behind this language is to take a few steps back and stop adding layer after layer to increase the level of the language and making development easier but instead it provide all the tools you need for doing it on your own. No extra layers, no hidden behavior. What you write is what you’re going to get.

Now, let’s suppose that we have function like this:

// services/users_service.go

func GetUser(userId int64) (*domain.User, error) {
    user, err := clients.Database.GetUser(userId)
    if err != nil {
        return nil, err
    }
    if user == nil {
        return nil, errors.New(fmt.Sprintf("user %d not found", userId))
    }
    return user, nil
}

As you can see, the function takes an int64 being the id of the user we want to get and then tries to fetch that ID from a given database. Pretty straightforward approach. Now, what if we want to have 100% coverage while testing this function? If we have 3 different independent paths then we’re going to need 3 different test cases. Let’s write this:

// services/users_service_test.go

package services

import(
    "testing"
)

func TestGetUserErrorFromDatabase(t *testing.T) {
    // Test first return.
}

func TestGetUserNotFound(t *testing.T) {
    // Test second return.
}

func TestGetUserNoError(t *testing.T) {
    // Test third return.
}

Or, if you’re using Go >= 1.7 you could use subtests instead:

// services/users_service_test.go

package services

import(
    "testing"
)

func TestGetUser(t *testing.T) {
    t.Run("ErrorFromDatabase", func(t *testing.T) {
        // Test first return.
    })

    t.Run("UserNotFound", func(t *testing.T) {
        // Test second return.
    })

    t.Run("NoError", func(t *testing.T) {
        // Test third return.
    })
}

As you can see, each test case will test each independent path you have in your GetUser function. In order to do this, you need to have complete control over what Database.GetUser() returns. How can we say to GetUser() that it should return an error, a nil user or an actual user? Let’s see how database is implemented:

// clients/database.go

var(
    Database = dbClient{}
)

type dbClient struct {}

func (c *dbClient) GetUser(id int64) (*domain.User, error) {
    // Fetchs the user from the actual database
    ...
    return &user, nil
}

When looking at how the database client is implemented we can see that we have a lot of issues here:

  • Database is of type dbClient being an struct. We have no way of mocking the struct behavior once it is defined.
  • Since we’re searching for the user in a running database then:
    • To test the error path we need to break the DB integration.
    • To test the happy path (user exists) we need to have a user in the DB.
  • This database client does not provide any mocking features out of the box.

We have a great opportunity for a refactor in the database client!! If we want to create unit tests, then you need to remember:

“Unit tests must NOT access any other artifact more than the one being tested”

This includes databases, the file system, HTTP calls, anything. Code under test should be isolated and tested unitary without any integration in place.

Let’s change our database client a little bit, shall we?

// clients/database.go

var(
    Database dbClient = dbClientImpl{}
)

type dbClient interface {
    GetUser(id int64) (*domain.User, error)
}

type dbClientImpl struct {}

func (c *dbClientImpl) GetUser(id int64) (*domain.User, error) {
    // Fetchs the user from the actual database
    ...
    return &user, nil
}

Now, looks pretty much the same but this version has lots of improvements over the previous one:

  • Database is of type dbClient being now an interface.
  • Database value is of type dbClientImpl being an struct implementing dbClient interface.

With these 2 little changes now the behavior we’re providing is via interfaces and not via structs. Why? Because now any struct could implement that interface if we need it! Let’s go back to our test case to see the difference.

// services/users_service_test.go

package services

import(
    "testing"
    "github.com/username/project/....../clients"
)

// This will be the mock for our database client.
// Since we want to mock the GetUser interface, we need to set a property in this struct
// that will be the function the mock calls when GetUser() is called.
type dbClientMock struct {
    getUserFn func(id int64) (*domain.User, error)
}

// In here we're implementing the dbClient interface defined in our clients/database.go:6
// Note that the signature of this method is exactly the same as the one defined in line 14
// Since dbClientMock has a property called getUserFn returning the same value as this method
// we can just return what that property returns.
func (c *dbClientMock) GetUser(id int64) (*domain.User, error) {
    return c.getUserFn(id)
}

func TestGetUser(t *testing.T) {
    // First we create a new mock for our database client:
    dbMock := dbClientMock{}

    // Since struct dbClientMock implements dbClient interface, this is valid!
    clients.Database = dbMock

    t.Run("ErrorFromDatabase", func(t *testing.T) {
        // Test first return.
    })

    t.Run("UserNotFound", func(t *testing.T) {
        // Test second return.
    })

    t.Run("NoError", func(t *testing.T) {
        // Test third return.
    })
}

Can you see the difference? What we did in here was creating a new struct called dbClientMock and we have made sure that this structure implements the dbClient interface. This interface requires any struct to implement a single method called GetUser(id int64) (*User, error) and that’s exactly what we did. So… Why would we ever need the getUserFn attribute then? Let’s look at how the code would be if we create a mocked implementation for the GetUser() method in our mock:

func (c *dbClientMock) GetUser(id int64) (*domain.User, error) {
    return nil, errors.New(fmt.Sprintf("user %d not found", id))
}

As you can see, if we use this function instead of the one we’ve defined before we’re going to be able to test only the user not found situation but none of the other two we need to have 100% coverage in our function. That is why we have an struct with a field being a function. Because we can change this field’s value and make the method’s behavior to change as we need to.

Finally, let’s see how we can use all of this in our test cases:

// services/users_service_test.go

package services

import(
    "testing"
    "..../clients" // This would be our DB client package
    "github.com/stretchr/testify/assert"
)

type dbClientMock struct {
    getUserFn func(id int64) (*domain.User, error)
}

func (c *dbClientMock) GetUser(id int64) (*domain.User, error) {
    return c.getUserFn(id)
}

func TestGetUser(t *testing.T) {

    dbMock := dbClientMock{}
    clients.Database = dbMock

    t.Run("ErrorFromDatabase", func(t *testing.T) {
        dbMock.getUserFn = func(id int64) (*domain.User, error) {
            return nil, errors.New("error connecting to our cool database")
        }

        user, err := GetUser(123)

        assert.Nil(t, user)
        assert.NotNil(t, err)
        assert.EqualValues(t, "error connecting to our cool database", err.Error())
    })

    t.Run("UserNotFound", func(t *testing.T) {
        dbMock.getUserFn = func(id int64) (*domain.User, error) {
            return nil, nil
        }

        user, err := GetUser(123)

        assert.Nil(t, user)
        assert.NotNil(t, err)
        assert.EqualValues(t, "user 123 not found", err.Error())
    })

    t.Run("NoError", func(t *testing.T) {
        dbMock.getUserFn = func(id int64) (*domain.User, error) {
            user := domain.User{
                Id: id,
                FirstName: "Cool",
                LastName: "Mock",
            }
            return &user, nil
        }

        user, err := GetUser(123)

        assert.Nil(t, err)
        assert.NotNil(t, user)
        assert.EqualValues(t, 123, user.Id)
        assert.EqualValues(t, "Cool", user.FirstName)
        assert.EqualValues(t, "Mock", user.LastName)
    })
}

As you can finally see, in each test case we modify what our mock’s method returns by relying on an internal field of the struct implementing the mock. If you want to change how the database behaves you just need to modify the mock behavior. Now any struct in any package implementing our database client interface can do this!

Hope you liked the article! If you have any questions, please let me know!

Fede.

1 thought on “How to mock behaviors in Go”

  1. Kuan-Chou Chen

    Hi,
    Thanks for the post, I got some questions here:
    I wonder the purpose of this code :clients.Database = dbMock ?
    Since I did not see any “clients.Database” usage after it, is it some kind of sanity check to see if we do implement GetUser() method as required by clients.Database here?
    Wouldn’t the test code still work without it? Thanks.

Leave a Comment