Pular para o conteúdo principal

victorstein.dev

61/100 Dias de Golang - Testes - Mocks

Table of Contents

# Testes - Mocks

Já escrevi algumas vezes sobre testes aqui no blog:

Mas está faltando falar sobre Mocks. Mocks são estruturas que simulam o comportamento de componentes externos (como bancos de dados, APIs, …) para testar a lógica de negócio sem depender desses serviços. Em Go podemos fazer isso de diversas formas. Uma delas é utilizando a biblioteca testify.

Vamos iniciar um projeto chamado mocktestes

go mod init mocktestes

Vamos aproveitar e já instalar a o teste o testify

go get github.com/stretchr/testify/mock

Veja a estrutura de arquivos que o projeto vai ficar:

.
├── go.mod
├── go.sum
├── main.go
├── models
│   └── user.go
├── repository
│   └── user_repository.go
└── service
    ├── user_service.go
    └── user_service_test.go

Vamos definir nosso models/user.go, será uma struct com os dados do usuário.

package models

type User struct {
	ID       int
	Name     string
	Email    string
	Active   bool
	CreateAt string
}

Na pasta repository teremos a lógica de acesso aos dados da aplicação. Para esse exemplo não vou implementar a lógica, vou só descrever os métodos. Essa implementação é especialmente útil quando queremos desacoplar a aplicação e preparar para os testes.

package repository

import (
	"errors"
	"mocktestes/models"
)

type UserRepository interface {
	GetUserByID(id int) (*models.User, error)
	SaveUser(user *models.User) error
	ListUsers() ([]*models.User, error)
}

type UserRepositoryImpl struct {
	// Aqui poderia ter uma conexão com banco de dados
	// db *sql.DB
}

func (r *UserRepositoryImpl) GetUserByID(id int) (*models.User, error) {
	// Implementação real
	return nil, errors.New("not implemented")
}

func (r *UserRepositoryImpl) SaveUser(user *models.User) error {
	return errors.New("not implemented")
}

func (r *UserRepositoryImpl) ListUsers() ([]*models.User, error) {
	// Implementação real
	return nil, errors.New("not implemented")
}

Na pasta service temos a camada de serviço da aplicação, que implementa a lógica de negócios do sistema. Nela temos uma API simplificada para camadas superiores.

package service

import (
	"mocktestes/models"
	"mocktestes/repository"
)

type UserService struct {
	repo repository.UserRepository
}

func NewUserService(repo repository.UserRepository) *UserService {
	return &UserService{
		repo: repo,
	}
}

func (s *UserService) GetUser(id int) (*models.User, error) {
	return s.repo.GetUserByID(id)
}

func (s *UserService) SaveUser(user *models.User) error {
	return s.repo.SaveUser(user)
}

func (s *UserService) ListUsers() ([]*models.User, error) {
	return s.repo.ListUsers()
}

Toda essa estrutura para mostrarmos como ficará os mocks no nosso arquivo user_service_test.go. Veja a estrutura dele passo a passo e no final o arquivo completo.

Aqui definimos uma estrutura MockUserRepository que incorpora mock.Mock que fornece funcionalidades para simular comportamentos.

type MockUserRepository struct {
    mock.Mock
}

Vou explicar uma implementação do GetUserByID as outras seguem um padrão similar. A ideia principal aqui é simular o comportamento de um repositório de usuários durante testes os testes, sem acessar o banco de dados real.

func (m *MockUserRepository) GetUserByID(id int) (*models.User, error) {
    args := m.Called(id)
    
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    
    return args.Get(0).(*models.User), args.Error(1)
}

m.Called(id) esse método é herdado da estrutura mock.Mock do pacote testify/mock. Ele registra que o método foi chamado com o argumento id. O retorno dele é um objeto do tipo mock.Arguments, que permite acessar os valores que foram “preparados” (ou configurados) para esse mock quando ele for chamado com aquele argumento.

args := m.Called(id)

Aqui pegamos o primeiro valor configurado para o mock e verificamos se é diferente de nil. Caso seja, retornamos um erro.

if args.Get(0) == nil {
    return nil, args.Error(1)
}

E por fim retornamos args.Get(0).(*models.User) converte o retorno configurado para o tipo esperado, que é o User. e aqui args.Error(1) retornamos o erro.

return args.Get(0).(*models.User), args.Error(1)

O que estamos fazemos aqui é montar o mock e os comportamento para no caso de cada entrada. Vamos enteder bem essa função quando formos fazer o teste. Veja como ele está definido:

func TestGetUser(t *testing.T) {
	// Arrange
	mockRepo := new(MockUserRepository)
	service := NewUserService(mockRepo)

	expectedUser := &models.User{
		ID:       1,
		Name:     "Test User",
		Email:    "test@example.com",
		Active:   true,
		CreateAt: "2023-01-01",
	}

	// Configurar o mock para retornar o usuário esperado
	mockRepo.On("GetUserByID", 1).Return(expectedUser, nil)

	// Configurar o mock para simular um erro
	mockRepo.On("GetUserByID", 999).Return(nil, errors.New("user not found"))

	// Act & Assert - Caso de sucesso
	user, err := service.GetUser(1)
	assert.NoError(t, err)
	assert.Equal(t, expectedUser, user)

	// Act & Assert - Caso de erro
	user, err = service.GetUser(999)
	assert.Error(t, err)
	assert.Nil(t, user)
	assert.Equal(t, "user not found", err.Error())

	// Verificar se todas as expectativas foram atendidas
	mockRepo.AssertExpectations(t)
}

Aqui definimos o mockRepo e passamos ele para o service e criamos uma usuário fake, que será o padrão:

mockRepo := new(MockUserRepository)
service := NewUserService(mockRepo)

expectedUser := &models.User{
	ID:       1,
	Name:     "Test User",
	Email:    "test@example.com",
	Active:   true,
	CreateAt: "2023-01-01",
}

Depois fazemos a configuração dos métodos de mock. Falamos que caso chamarmos a função: "GetUserByID" com o parâmetro 1, devemos dar o retorno: expectedUser, nil e fazemos a mesma definição para outro Id.

// Configurar o mock para retornar o usuário esperado
mockRepo.On("GetUserByID", 1).Return(expectedUser, nil)

// Configurar o mock para simular um erro
mockRepo.On("GetUserByID", 999).Return(nil, errors.New("user not found"))

E depois rodamos o teste mesmo:

// Act & Assert - Caso de sucesso
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)

// Act & Assert - Caso de erro
user, err = service.GetUser(999)
assert.Error(t, err)
assert.Nil(t, user)
assert.Equal(t, "user not found", err.Error())

// Verificar se todas as expectativas foram atendidas
mockRepo.AssertExpectations(t)

O mock não garante que a implementação real está correta. Ele apenas serve para testar a lógica do código que consome aquele método. Um mock simula o comportamento de um componente. Para garantir que a implementação real do método corresponde ao que o mock está simulando, você precisa de testes de integração