61/100 Dias de Golang - Testes - Mocks
Table of Contents
#
Testes - Mocks
Já escrevi algumas vezes sobre testes aqui no blog:
- 25/100 Dias de Golang - Testes em Golang
- 26/100 Dias de Golang - Testes em batch, AAA e Cobertura de Código
- 27/100 Dias de Golang - Testes de benchmark
- 28/100 Dias de Golang - Testes em Go com Fuzzing
- 29/100 Dias de Golang - Testes com Testify
- 30/100 Dias de Golang - Testes - Exemplo Prático
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