Mocking interfaces com gomock ou testify
1. Por que mockar interfaces em Golang?
Testes unitários em Go precisam ser rápidos, previsíveis e isolados. Quando seu código depende de serviços externos — banco de dados, APIs HTTP, cache Redis ou envio de emails — você não quer que seus testes dependam desses sistemas estarem disponíveis. Mockar interfaces resolve esse problema.
A filosofia de composição do Go, baseada em interfaces pequenas e coesas, torna o mocking natural. Ao definir uma interface como contrato, você pode substituir implementações reais por mocks durante os testes. Os benefícios são claros:
- Velocidade: testes que não tocam em I/O rodam em milissegundos
- Previsibilidade: você controla exatamente o que cada dependência retorna
- Sem efeitos colaterais: nenhum dado real é alterado, nenhum email é enviado
2. Fundamentos: interfaces e injeção de dependência
Antes de mockar, precisamos de interfaces bem definidas e injeção de dependência. Veja um exemplo mínimo de repositório de usuários:
type User struct {
ID int
Name string
Email string
}
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
A injeção via construtor (NewUserService) permite que passemos qualquer implementação de UserRepository — real ou mock.
3. Mocking com gomock (geração de código)
O gomock gera código automaticamente a partir de suas interfaces. Instalação:
go install github.com/golang/mock/mockgen@latest
Gere o mock para nossa interface:
mockgen -source=repository.go -destination=mocks/mock_repository.go -package=mocks
Agora, um teste prático simulando um serviço de envio de email:
// email.go
type EmailSender interface {
Send(to, subject, body string) error
}
type NotificationService struct {
sender EmailSender
}
func NewNotificationService(sender EmailSender) *NotificationService {
return &NotificationService{sender: sender}
}
func (s *NotificationService) NotifyUser(user *User, message string) error {
return s.sender.Send(user.Email, "Notificação", message)
}
Teste com gomock:
// email_test.go
func TestNotifyUser_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockSender := mocks.NewMockEmailSender(ctrl)
mockSender.EXPECT().
Send("user@example.com", "Notificação", "Bem-vindo!").
Return(nil).
Times(1)
service := NewNotificationService(mockSender)
user := &User{Email: "user@example.com"}
err := service.NotifyUser(user, "Bem-vindo!")
assert.NoError(t, err)
}
EXPECT() define o que esperamos, Return() especifica o retorno, Times() controla quantas vezes a chamada deve ocorrer.
4. Testify/mock: mocking manual sem geração
O testify oferece uma abordagem mais manual, sem geração de código. Você cria um struct que embute mock.Mock e implementa a interface:
type MockCache struct {
mock.Mock
}
func (m *MockCache) Get(key string) (string, error) {
args := m.Called(key)
return args.String(0), args.Error(1)
}
func (m *MockCache) Set(key, value string) error {
args := m.Called(key, value)
return args.Error(0)
}
Teste mockando um cache Redis:
func TestGetFromCache(t *testing.T) {
mockCache := new(MockCache)
mockCache.On("Get", "user:123").Return("cached_value", nil)
mockCache.On("Set", "user:123", "new_value").Return(nil)
service := NewCacheService(mockCache)
result, err := service.GetOrSet("user:123", "new_value")
assert.NoError(t, err)
assert.Equal(t, "cached_value", result)
mockCache.AssertExpectations(t)
}
On() define o comportamento esperado, AssertExpectations(t) verifica se todas as chamadas ocorreram conforme planejado.
5. Configurando expectativas e asserções
Ambas as bibliotecas permitem controle fino sobre as chamadas.
Gomock — exemplo com logger e chamadas sequenciais:
func TestLogger_SequentialCalls(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := mocks.NewMockLogger(ctrl)
gomock.InOrder(
mockLogger.EXPECT().Info("iniciando processo").Return(),
mockLogger.EXPECT().Debug("etapa 1 concluída").Return(),
mockLogger.EXPECT().Info("processo finalizado").Return(),
)
processor := NewProcessor(mockLogger)
processor.Run()
}
Testify — verificando argumentos específicos:
func TestLogger_WithArgs(t *testing.T) {
mockLogger := new(MockLogger)
mockLogger.On("Info", mock.MatchedBy(func(msg string) bool {
return strings.Contains(msg, "error")
})).Return()
service := NewService(mockLogger)
service.HandleError(errors.New("conexão falhou"))
mockLogger.AssertCalled(t, "Info", mock.AnythingOfType("string"))
}
6. Tratamento de erros e cenários negativos
Simular falhas é essencial para testar caminhos alternativos:
// Gomock
mockDB.EXPECT().Query("SELECT ...").Return(nil, errors.New("conexão perdida"))
// Testify
mockDB.On("Query", "SELECT ...").Return(nil, errors.New("conexão perdida"))
Teste de fallback com retry:
func TestRepository_RetryOnFailure(t *testing.T) {
mockDB := new(MockDatabase)
mockDB.On("FindByID", 1).Return(nil, errors.New("timeout")).Once()
mockDB.On("FindByID", 1).Return(&User{ID: 1, Name: "João"}, nil).Once()
repo := NewUserRepository(mockDB)
user, err := repo.FindByIDWithRetry(1)
assert.NoError(t, err)
assert.Equal(t, "João", user.Name)
mockDB.AssertExpectations(t)
}
7. Boas práticas e armadilhas comuns
- Evite mocks desnecessários: para dados simples, use stubs (implementações fixas) ou fakes (implementações funcionais leves)
- Mantenha mocks atualizados: quando a interface muda, os mocks precisam ser regenerados (gomock) ou atualizados (testify)
- Nunca mocke structs concretos: apenas interfaces. Mockar tipos concretos quebra o propósito do desacoplamento
- Automatize com
go:generate:
//go:generate mockgen -source=repository.go -destination=mocks/mock_repository.go -package=mocks
type UserRepository interface { ... }
Execute go generate ./... para regenerar todos os mocks de uma vez.
8. Comparação: gomock vs testify – quando usar cada um?
| Aspecto | Gomock | Testify |
|---|---|---|
| Geração de código | Automática (mockgen) | Manual |
| Curva de aprendizado | Média (sintaxe EXPECT()) |
Baixa (métodos On/Return) |
| Projetos grandes | Excelente (muitas interfaces) | Pode ficar verboso |
| Times pequenos | Overhead desnecessário | Ideal |
| Performance | Ligeiramente mais rápido | Similar |
| Manutenção | Regenerar mocks ao mudar interface | Atualizar manualmente |
Quando usar gomock: projetos com dezenas de interfaces, times maiores, necessidade de geração automática e controle fino de ordem de chamadas.
Quando usar testify: protótipos, times pequenos, simplicidade, ou quando você prefere código explícito sem geração.
Refatorando um teste de gomock para testify:
// Gomock
mockSender.EXPECT().Send("a@b.com", "subject", "body").Return(nil)
// Testify
mockSender.On("Send", "a@b.com", "subject", "body").Return(nil)
A diferença é sutil, mas o testify elimina a necessidade do ctrl e do defer ctrl.Finish().
Conclusão
Mockar interfaces em Go é uma prática essencial para testes unitários robustos. Tanto gomock quanto testify cumprem bem esse papel — a escolha depende do contexto do seu projeto. Comece com testify para simplicidade, migre para gomock quando a escala exigir automação. Em ambos os casos, lembre-se: mocks são ferramentas para testar comportamento, não implementação.
Referências
- Documentação oficial do gomock — Repositório oficial com guia de instalação, geração de mocks e exemplos detalhados
- Testify - pacote mock — Documentação completa do testify/mock com exemplos de uso e API de asserções
- Go Mock (gomock) tutorial - DigitalOcean — Tutorial prático cobrindo desde instalação até cenários avançados de mocking
- Testify mock examples - Medium — Exemplos detalhados de mocking com testify, incluindo matched arguments e cenários de erro
- Effective Go - Interfaces — Documentação oficial sobre interfaces em Go, fundamento teórico para mocking
- Go Testing Patterns - The Go Blog — Padrões de teste em Go, incluindo boas práticas para mocks e subtests