Test with Testify and Mockery in Go

Authors
  • Amit Shekhar
    Name
    Amit Shekhar
    Published on
Test with Testify and Mockery in Go

I am Amit Shekhar, Co-Founder @ Outcome School, I have taught and mentored many developers, and their efforts landed them high-paying tech jobs, helped many tech companies in solving their unique problems, and created many open-source libraries being used by top companies. I am passionate about sharing knowledge through open-source, blogs, and videos.

In this blog, we will learn about writing the unit test with Testify and Mockery in Go (Golang) project.

These two packages are very useful during the testing of any Go files.

  • testify: A toolkit with common assertions and mocks that plays nicely with the standard library.
  • mockery: A mock code autogenerator for Golang used in testing.

As we know that the testify package is used for the purpose of the assertions.

mockery package provides the ability to easily generate mocks for Golang interfaces using the testify package. It removes the boilerplate coding required to use mocks.

So, let's start learning to use both packages to write tests in Go.

I will be using the below-mentioned project for the implementation part. The project follows a clean architecture in Go Language. You can find the complete code for unit testing using the Testify and Mockery mentioned in the blog in the project itself.

Link to the project: Go Backend Clean Architecture.

In the project, we have Controller, Usecase, and Repository. In this blog, we will learn to write the test by taking the Usecase as an example.

Consider the TaskUsecase present inside the given project.

package usecase

import (
	"context"
	"time"

	"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
)

type taskUsecase struct {
	taskRepository domain.TaskRepository
	contextTimeout time.Duration
}

func NewTaskUsecase(taskRepository domain.TaskRepository, timeout time.Duration) domain.TaskUsecase {
	return &taskUsecase{
		taskRepository: taskRepository,
		contextTimeout: timeout,
	}
}

func (tu *taskUsecase) Create(c context.Context, task *domain.Task) error {
	ctx, cancel := context.WithTimeout(c, tu.contextTimeout)
	defer cancel()
	return tu.taskRepository.Create(ctx, task)
}

func (tu *taskUsecase) FetchByUserID(c context.Context, userID string) ([]domain.Task, error) {
	ctx, cancel := context.WithTimeout(c, tu.contextTimeout)
	defer cancel()
	return tu.taskRepository.FetchByUserID(ctx, userID)
}

So, our aim is to test the Usecase that is dependent on Repository. We will have to mock the Repository.

As the mockery generates the mocks for Golang interfaces, suppose we have the following interface for the TaskRepository.

type TaskRepository interface {
	Create(c context.Context, task *Task) error
	FetchByUserID(c context.Context, userID string) ([]Task, error)
}

As a next step, we need to run the following command to generate the mock for this repository.

mockery --dir=domain --output=domain/mocks --outpkg=mocks --all

Note: You will have to modify the command based on your project structure and directory.

It should generate the TaskRepository mock that we will be using for the testing.

Now, that mock for the Repository is also ready, we can write the test for the Usecase.

We will write the test for the FetchByUserID method present in the Usecase.

Here, we are testing two cases:

  • Success: The repository returns data successfully without any errors.
  • Error: The repository returns an error.

The following is the code showing how to write those tests using the testify and mockery:

package usecase_test

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
	"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/mocks"
	"github.com/amitshekhariitbhu/go-backend-clean-architecture/usecase"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"go.mongodb.org/mongo-driver/bson/primitive"
)

func TestFetchByUserID(t *testing.T) {
	mockTaskRepository := new(mocks.TaskRepository)
	userObjectID := primitive.NewObjectID()
	userID := userObjectID.Hex()

	t.Run("success", func(t *testing.T) {

		mockTask := domain.Task{
			ID:     primitive.NewObjectID(),
			Title:  "Test Title",
			UserID: userObjectID,
		}

		mockListTask := make([]domain.Task, 0)
		mockListTask = append(mockListTask, mockTask)

		mockTaskRepository.On("FetchByUserID", mock.Anything, userID).Return(mockListTask, nil).Once()

		u := usecase.NewTaskUsecase(mockTaskRepository, time.Second*2)

		list, err := u.FetchByUserID(context.Background(), userID)

		assert.NoError(t, err)
		assert.NotNil(t, list)
		assert.Len(t, list, len(mockListTask))

		mockTaskRepository.AssertExpectations(t)
	})

	t.Run("error", func(t *testing.T) {
		mockTaskRepository.On("FetchByUserID", mock.Anything, userID).Return(nil, errors.New("Unexpected")).Once()

		u := usecase.NewTaskUsecase(mockTaskRepository, time.Second*2)

		list, err := u.FetchByUserID(context.Background(), userID)

		assert.Error(t, err)
		assert.Nil(t, list)

		mockTaskRepository.AssertExpectations(t)
	})

}

Case 1: Success

In this case, we have mocked the method of Repository to return success with the data.

Then, we are passing the mocked Repository into the Usecase.

After that, we are calling the method of the Usecase.

And then finally, asserting to check whether everything is as expected or not.

Case 2: Error

In this case, we have mocked the method of Repository to return an error.

Then, we are passing the mocked Repository into the Usecase.

After that, we are calling the method of the Usecase.

And then finally, asserting to check whether everything is as expected or not.

Now, our test is ready to be run. We can run the test and check.

We can run all the tests with the following command:

go test ./...

This is how we can write the unit test by using the Testify and Mockery package in Go (Golang) project.

I will highly recommend going through the project and checking the tests written for the Controller, Usecase, and Repository.

Link to the project: Go Backend Clean Architecture.

That's it for now.

Thanks

Amit Shekhar
Co-Founder @ Outcome School

You can connect with me on:

Follow Outcome School on:

Read all of our high-quality blogs here.