Creating test mocks using GoMock

Elton Minetto - Dec 20 '19 - - Dev Community

Using mocks in test development is a concept used in the vast majority of programming languages. In this post, I will talk about one solution to implement mocks in Go: GoMock.

To show GoMock functionality, I will use the tests created in my repository about Clean Architecture

Clean Architecture encourages testing across all layers, so it’s easy to see where we can use mocks to make development easier. As we write unit tests for the UseCases layer we are sure that the logic in this layer is covered by testing. In the Controller layer, we can use mocks to simulate the use of UseCases since we know that their functionality is already validated.

Let's create mocks for this layer, which is represented by the interfaces:

package bookmark

import "github.com/eminetto/clean-architecture-go/pkg/entity"

//Reader interface
type Reader interface {
    Find(id entity.ID) (*entity.Bookmark, error)
    Search(query string) ([]*entity.Bookmark, error)
    FindAll() ([]*entity.Bookmark, error)
}

//Writer bookmark writer
type Writer interface {
    Store(b *entity.Bookmark) (entity.ID, error)
    Delete(id entity.ID) error
}

//Repository repository interface
type Repository interface {
    Reader
    Writer
}

//UseCase use case interface
type UseCase interface {
    Reader
    Writer
}
Enter fullscreen mode Exit fullscreen mode

To make GoMock easier to use let's change the Makefile file to add mock generation functionality from the interfaces::

build-mocks:
  @go get github.com/golang/mock/gomock
  @go install github.com/golang/mock/mockgen
  @~/go/bin/mockgen -source=pkg/bookmark/interface.go -destination=pkg/bookmark/mock/bookmark.go -package=mock
Enter fullscreen mode Exit fullscreen mode

These commands download the gomock package and also the mockgen binary, which is used to generate the mocks. The command make build-mocks will generate the file pkg/bookmark/mock/bookmark.go, with the functions we will use in the tests. It is important to remember that whenever you change the interfaces of the pkg/bookmark/interface.go file, you have to run this command to update the mocks.

Let's now change one of the existing tests to make use of the mock. In the file api/handler/bookmark_test.go we will change the test TestBookmarkIndex. The original code was:

func TestBookmarkIndex(t *testing.T) {
  repo := bookmark.NewInmemRepository()
  service := bookmark.NewService(repo)
  r := mux.NewRouter()
  n := negroni.New()
  MakeBookmarkHandlers(r, *n, service)
  path, err := r.GetRoute("bookmarkIndex").GetPathTemplate()
  assert.Nil(t, err)
  assert.Equal(t, "/v1/bookmark", path)
  b := &entity.Bookmark{
    Name:        "Elton Minetto",
    Description: "Minetto's page",
    Link:        "http://www.eltonminetto.net",
    Tags:        []string{"golang", "php", "linux", "mac"},
    Favorite:    true,
  }
  _, _ = service.Store(b)
  ts := httptest.NewServer(bookmarkIndex(service))
  defer ts.Close()
  res, err := http.Get(ts.URL)
  assert.Nil(t, err)
  assert.Equal(t, http.StatusOK, res.StatusCode)
}
Enter fullscreen mode Exit fullscreen mode

And the new code is:

func TestBookmarkIndex(t *testing.T) {
  controller := gomock.NewController(t)
  defer controller.Finish()
  service := mock.NewMockUseCase(controller)
  r := mux.NewRouter()
  n := negroni.New()
  MakeBookmarkHandlers(r, *n, service)
  path, err := r.GetRoute("bookmarkIndex").GetPathTemplate()
  assert.Nil(t, err)
  assert.Equal(t, "/v1/bookmark", path)
  b := &entity.Bookmark{
    Name:        "Elton Minetto",
    Description: "Minetto's page",
    Link:        "http://www.eltonminetto.net",
    Tags:        []string{"golang", "php", "linux", "mac"},
    Favorite:    true,
  }
  service.EXPECT().
    FindAll().
    Return([]*entity.Bookmark{b}, nil)
  ts := httptest.NewServer(bookmarkIndex(service))
  defer ts.Close()
  res, err := http.Get(ts.URL)
  assert.Nil(t, err)
  assert.Equal(t, http.StatusOK, res.StatusCode)
}
Enter fullscreen mode Exit fullscreen mode

The changes were in the service creation, where we are now using the mock:

controller := gomock.NewController(t)
defer controller.Finish()
service := mock.NewMockUseCase(controller)
Enter fullscreen mode Exit fullscreen mode

We removed the line _, _ = service.Store(b) because we no longer need to include a record before using it. And we include the mock setup:

service.EXPECT().
  FindAll().
  Return([]*entity.Bookmark{b}, nil)
Enter fullscreen mode Exit fullscreen mode

This way the mock will behave as expected. So we can focus on testing only what interests us in this layer, which is the logic of the http handler such as handling of request and response, routes.

Also, we need to import the packages:

"github.com/eminetto/clean-architecture-go/pkg/bookmark/mock"
"github.com/golang/mock/gomock"
Enter fullscreen mode Exit fullscreen mode

In the repository you can see the other tests.

Using mock is not a consensus in the development community, with some people supporting and others pointing out problems in some approaches. I have been using this technique lately and enjoying the result as it helps keep the tests more focused. This can avoid re-testing things that have already validated with unit tests in other layers. It is also useful when we need to emulate code access to an external micro service, library, or resource.

Since all standard Go libraries extensively use interfaces, you can create mocks for many resources such as files, databases. That’s why I believe that solutions like GoMock can be very useful in projects of various sizes.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .