Generating Mock Versions of Interfaces in Go

Generating Mock Versions of Interfaces in Go

I've been a big fan of using Testify's mocking capabilities and it has made building unit tests really easy.

The one piece that I feel like is missing is the autogeneration of a basic mock given an interface. Building these out by hand can be cumbersome especially when the contract changes.

I recently ran across the Counterfeiter library that does just that. It allows us to generate a fake implementation for an interface with some basic mocking capabilities that allow you to control what's returned when the fake implementation is called.

Problem

Let's say we're building a book store and we have a single service that pulls information from a data provider.

The only requirement we have is to swallow any errors returned by the data provider and just return the books we find instead of propagating up those errors. Maybe we have a really flaky old database?

How can we test out that our logic is correct given the data provider interface looks like this:

type BookDataProvider interface {
	GetBook(isbn string) (*model.Book, error)
}
This code resides in data/data.go

Service Logic

Let's say you were able to complete the book service and error handling logic like so:

type BookService interface {
	GetBooks(isbns []string) []*model.Book
}

type service struct {
	provider data.BookDataProvider
}

func (s *service) GetBooks(isbns []string) []*model.Book {
	books := make([]*model.Book, 0)
	for _, isbn := range isbns {
		if book, err := s.provider.GetBook(isbn); err == nil {
			books = append(books, book)
		}
	}
	return books
}

func NewService(provider data.BookDataProvider) BookService {
	return &service{
		provider: provider,
	}
}

Now, how do we go about testing this out and making sure it's working as expected? This is where generating fake implementations comes in handy.

Generating Fake Implementations

In order to generate fake implementations of the data provider, we need to add the Counterfeiter library as a dependency of our Go module which we can do by executing this in the root of our project:

go get -u github.com/maxbrunsfeld/counterfeiter/v6

You should see now that the Counterfeiter library has been added to your go.mod file and we're ready to go.

All we have to do now is add these comments above the interfaces we're looking to mock. For the data provider it would look like this:

//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -o ./fakes . BookDataProvider
type BookDataProvider interface {
	GetBook(isbn string) (*model.Book, error)
}

What this is saying is to create a fake implementation of the data provider and output that to a fakes package relative to the current package. So when we run this:

go generate ./...

We should now see that our fake implementation has been generated and has been created in the path we expected:

├── data
│   ├── data.go
│   └── fakes
│       └── fake_book_data_provider.go
├── go.mod
├── go.sum
├── main.go
├── model
│   └── model.go
└── service
    ├── service.go
    └── service_test.go

Happy Path

In order to test out the case where our data provider isn't throwing errors, we should expect to get one book back per ISBN:

func TestBookServiceHappyPath(t *testing.T) {

	// Given
	mockBook := &model.Book{}
	provider := &fakes.FakeBookDataProvider{}
	provider.GetBookReturns(mockBook, nil)
	service := NewService(provider)

	// When
	books := service.GetBooks([]string{"123", "456"})

	// Then
	if len(books) != 2 {
		t.Errorf("The service should have returned 2 books, but instead returned %d books", len(books))
	}
}

The code above creates an instance of the fake data provider and then instructs it to always return the fake book along with a nil error. So anytime we call the data provider in our loop, it will return that pair of values and should end up with returning 2 books from the service.

Sad Path

For the cases where our provider is returning errors, we can test for that as well using some built-in methods that come with the generated code:

func TestBookServiceSadPath(t *testing.T) {

	// Given
	provider := &fakes.FakeBookDataProvider{}
	provider.GetBookReturns(nil, data.ErrBadConnection)
	service := NewService(provider)

	// When
	books := service.GetBooks([]string{"123", "456"})

	// Then
	if len(books) != 0 {
		t.Errorf("The service should have returned 0 books, but instead returned %d books", len(books))
	}
}

Again, we create an instance of the fake data provider and instruct it to always return a nil book and bad connection error. So, anytime we call the provider from the service, we'll receive this error and our service should an empty slice of books.

There are other functions in the mocked interfaces that we can test, but I'll follow up with another post on that soon.

Reference

Counterfeiter library - https://github.com/maxbrunsfeld/counterfeiter