Developers , Apps

Go test mocks: interfaces to the rescue

Developing an application can be something complex, specially if the domain has a lot of intricate business rules. When it comes to writing tests, it is important that these are effective and simple. We want to be able to cover our code’s behavior as best as possible while keeping things relatively simple for the sake ...

Developing an application can be something complex, specially if the domain has a lot of intricate business rules. When it comes to writing tests, it is important that these are effective and simple. We want to be able to cover our code’s behavior as best as possible while keeping things relatively simple for the sake of maintainability and extensibility. This post specifically covers the topic of unit tests and an approach to package/module testing, focused on making mocks easier.

The Problem

When we develop packages within our Go project, sometimes we decide to create an interface before we actually need it. This type of interface is called preemptive. And why is this not a good pratice in Go?

Since Go has implicit interfaces, there is no need to explicitly declare the intent of implementing it in our structs (no “implements” keyword like Java, for example).

This also means that interfaces can be created in a different package than the package type that’s originally implementing it, which can save us when mocking our data.

Example: You have a repository method called Insert(data Data) error that comes from a specific library that you are using but you didn’t write it. Now you want to unit test your code, what should you do ? You just need to make an interface to replace your repository method.

JavaScript
type Inserter interface {
	Insert(data Data) error
}

type UserRepository struct {
    databaseClient someDatabaseClient
}
// implicitly implementing the method
func (repo *UserRepository) Insert(data Data) error {...}

Now you can make any mock object you want.  That’s the premise of implicit interfaces. The only thing that matters is that the repository has the Insert method, and now, the repository is implementing the interface.

The Go’s power is in its small interfaces allowing its reuse. When you are coding Preemptive Interface it tend to have countless methods which makes it even more difficult to do exchangeable implementations and mock our data.

Practical Example

JavaScript
// src/repository/user/users.go
type RepositoryImplement struct {
	db db.DB
}

type UserRepositoryI interface {
    GetAll() ([]model, error)
    GetByEmail(email string) (model, error)
    GetById(id string) (model, error)
    Insert(payload dto.Payload) error
    ...
}
func (repo *RepositoryImplement) GetAll() ([]model, error) {...}
func (repo *RepositoryImplement) GetByEmail(email string) (model, error) {...}
func (repo *RepositoryImplement) GetById(id string) (model, error) {...}
func (repo *RepositoryImplement) Insert(payload dto.Payload) error {...}

...

The problem with this is the more your application grows, the more methods are inserted in the interface, which will generate a huge interface and consequently we will have a lot of unnecessary mocks.

Let’s say that your service only uses one or two methods from your repository, like:

JavaScript
// src/service/user/users.go
import (usersRepository "src/repository/users")

type UserService struct {
    repository usersRepository.RepositoryI
}

func NewService(repository usersRepository.RepositoryI) *UserService {
	return &UserService {
		repository: repository
	}
}


func (u *UserService) SignUp(payload dto.PayloadSignUp) error {
	user, err := u.repository.GetByEmail(payload.Email)
	//...validations
	
	err = u.repository.Insert(payload)
	if err != nil {
	///...validations
	}
}

Therefore, comes the part where you will test your service. The first thing you are going to do is mock your repository and there comes our problem.

When we are doing the mock, we’ll need to implement all functions from our interface to instantiate our service, which is not something ideal, since we have a lot of them in our repository.

JavaScript
// src/service/user/mocks.go
type MyMockDB struct {
}

func (m *MyMockDB) GetAll() ([]model, error) {}
func (m *MyMockDB) GetById(id string) (model, error) {}
func (m *MyMockDB) GetByEmail(id string) (model, error) {}
func (m *MyMockDB) Func1(id string) (model, error) {}
func (m *MyMockDB) Func2(id string) (model, error) {}
func (m *MyMockDB) Func3(id string) (model, error) {}
...

When we do this implementation, we are implementing functions that are not even used by our service, but as they are all grouped in a same interface, we need to implement them all.

JavaScript
// nothing changes here, it is only an "how to" example. 
// src/service/user/user_test.go
import (
    "testing"
    "src/repositories/users/mocks"
)

func TestSignUp(t *testing.T) {
    mockDb := &mocks.MyMockDb{}
    userService := NewService(mockDb)
    
    ...
    ...test implementation
}

How to avoid this simple “mistake”

The insight is the way how we implement these methods and/or where we create the interface. When we do our repository implementation, instead of creating an interface in the data layer, we will create an interface in our service layer that contains only the methods that we’ll use.

JavaScript
// src/services/users/users.go
type UserI interface {
    GetByEmail(email string) (model, error)
    Insert(payload dto.Payload) error
}

type UserService struct {
    UserRepository UserI
}

func NewService(repository UserI) *UserService {
	return &UserService {
		repository: repository
	}
}

    
func (u *UserService) SignUp(payload dto.PayloadSignUp) error {
	user, err := u.repository.GetByEmail(payload.Email)
	//...validations
	
	err = u.repository.Insert(payload)
	if err != nil {
	///...validations
	}
}

Now, we have an easy way to mock only the functions that we need, without implementing all those unused functions.

JavaScript
// src/repository/user/mock.go
type MockUserRepository struct {
    GetAllFunc func() ([]model, error)
    InsertFunc func(payload dto.Payload) error
}

func (m *MockUserRepository) GetAll() ([]model, error) {
    return m.GetAllFunc()
}
func (m *MockUserRepository) Insert(payload dto.Payload) error {
    return m.InsertFunc(payload)
}


// src/service/user/user_test.go
import (
    "testing"
    "src/repositories/users/mocks"
)

func TestSignUp(t *testing.T) {
    mockDb := &mocks.MyMockDb{}
    userService := NewService(mockDb)
    
    t.Run("happy path", func(t *testing.T) {
	    mockDb.GetAllFunc = func() ([]model, error) {
		    return ...(data you want return)
	    }
	    
	    ...
	    ...test implementation
    })
}

The bigger the project becomes, the more difficult it becomes to maintain, so it’s important to think outside the box when designing API’s.

Careful with abstractions

As we can see, it’s trivial to replace the implementation of a method (with a mock, for example) by simply creating a different implementation that obeys the desired interface, but this is way easier if the interface is defined on the client/consumer level and not on the dependency itself.

Furthermore, when we are writing and designing our API the simple can be complex when we talk about abstractions, so here are some things that will help your tests:

  • Keep interfaces small
  • Remove unnecessary abstractions
  • Understand that Go allows you treat different types as same data type since it obeys the same interface

There we go! Now we can build our services thinking of our mocks andimplement it easily.

Related Articles

Developers Engineer Write Up

Deploying NestJS Microservices to AWS ECS with Pulumi IaC

Let’s see how we can deploy NestJS microservices with multiple environments to ECS using...

Read more
CI/CD
Developers DevOps

What is CI/CD? A Guide to Continuous Integration & Continuous Delivery

Learn how CI/CD can improve code quality, enhance collaboration, and accelerate time-to-ma...

Read more
AI Developers Engineer Write Up

Build a Powerful Q&A Bot with LLama3, LangChain & Supabase (Local Setup)

Harness the power of LLama3 with LangChain & Supabase to create a smart Q&A bot. This guid...

Read more
Demystifying AI
AI Developers

Demystifying AI: The Power of Simplification

Unleash AI's power without the hassle. Learn how to simplify complex AI tasks through easy...

Read more