Mocks e dublês de teste em Go
Como criar fakes, stubs e spies manualmente e usar o testify/mock para isolar regras de negócio de dependências lentas ou externas.
Publicada em · 10 min de leitura
Ao testar regras de negócio no backend, é comum nos depararmos com dependências que realizam operações lentas, caras ou com efeitos colaterais irreversíveis no mundo real: debitar um cartão de crédito em um gateway externo, enviar um SMS via Twilio, gravar registros em um cluster de banco de dados ou despachar eventos para o Apache Kafka.
Testes unitários confiáveis não devem depender de internet, bancos de dados ativos ou serviços de terceiros. Para isolar o código sob teste, utilizamos Dublês de Teste (Test Doubles).
Em Go, a abordagem para dublês de teste é muito diferente da maioria das linguagens corporativas. O Go não possui reflexão dinâmica permissiva nem frameworks que alteram o bytecode em tempo de execução (como Mockito no Java ou unittest.mock no Python). Em vez disso, o Go utiliza seu sistema de interfaces implícitas no ponto de consumo para permitir dublês leves, seguros em tempo de compilação e livres de mágica.
Esta aula ensina a criar dublês manuais puros (Fakes, Stubs, Spies) e mocks rigorosos com o pacote profissional testify/mock, além de apresentar a geração automatizada com mockgen.
O Mapa da Aula
| Parte | Seção | O que você vai dominar |
|---|---|---|
| 1. Filosofia | 1 e 2 | Interfaces no ponto de consumo e a taxonomia oficial dos 5 tipos de dublês de teste |
| 2. Dublês Manuais | 3 e 4 | Fakes em memória, Stubs de erro e Spies com contadores usando apenas Go puro |
| 3. Testify Mock | 5 a 7 | Struct embedding de mock.Mock, configuração com On(), casadores flexíveis e AssertExpectations |
| 4. Geração Automática | 8 | Quando e como automatizar a criação de mocks com mockgen e diretivas //go:generate |
| 5. Código de Produção | 9 e 10 | Caso prático completo de Checkout bancário, checklist de qualidade e armadilhas frequentes |
Pré-requisitos recomendados: Table-Driven Tests e Paralelismo e Suíte de Testes com Testify.
Parte 1: A Filosofia de Mocks em Go
1. Interfaces no ponto de consumo
Em linguagens como Java ou C#, é praxe o pacote produtor declarar uma interface gigante:
// ❌ Anti-padrão em Go: Interface monolítica declarada pelo provedor
package postgres
type RepositorioCompleto interface {
BuscarPorID(id string) (*User, error)
BuscarPorEmail(email string) (*User, error)
Salvar(u *User) error
Deletar(id string) error
ListarTodos() ([]*User, error)
// ...mais 15 métodos
}
Se o seu caso de uso precisa apenas verificar se um e-mail já existe, você é obrigado a mockar todos os 20 métodos dessa interface para conseguir rodar um teste.
Em Go idiomático, quem declara a interface é quem consome o serviço, e ela deve conter apenas os métodos estritamente necessários para aquela operação:
// Idiomático: Interface minúscula no ponto de consumo (Use Case)
package cadastro
type ValidadorExistencia interface {
ExistePorEmail(email string) (bool, error)
}
type ServicoCadastro struct {
repo ValidadorExistencia
}
Qualquer struct que implemente ExistePorEmail(email string) (bool, error) satisfaz esse contrato automaticamente, sem acoplamento. Mockar essa dependência exige implementar apenas um único método.
2. A Taxonomia dos 5 Dublês de Teste
O termo “Mock” costuma ser usado genericamente para qualquer substituto de teste. No entanto, o engenheiro Gerard Meszaros formalizou uma distinção clara entre 5 tipos de dublês:
- Dummy: Passado apenas para satisfazer a assinatura de parâmetros obrigatórios, mas nunca é acessado ou invocado pela lógica testada (ex:
nilou uma struct vazia). - Stub: Fornece respostas enlatadas e pré-programadas para chamadas específicas (ex: sempre retorna
(50.0, nil)ou sempre retornaErrConexao). - Fake: Possui uma implementação funcional real, porém muito simplificada e inadequada para produção (ex: um repositório que usa um
map[string]*Pedidoem memória em vez de um banco SQL real). - Spy: Envolve ou substitui um componente e registra silenciosamente o histórico das chamadas (quantas vezes foi chamado, quais argumentos recebeu) para validação posterior.
- Mock: Pré-programado com expectativas estritas de comportamento (quais métodos devem ser chamados, com quais argumentos e quantas vezes). Se o código fizer uma chamada inesperada ou esquecer uma chamada planejada, o teste falha.
Parte 2: Dublês Manuais com Go Puro (Zero Dependências)
Muitas vezes, a melhor solução em Go não exige nenhuma biblioteca externa. Criar seus próprios Fakes e Spies com structs simples é rápido, transparente e ajuda na manutenção contínua.
Veja a implementação de um Fake e um Spy manuais:
// Fake: repositório em memória com slice
type FakePedidoRepo struct {
pedidosSalvos []*Pedido
}
func (f *FakePedidoRepo) Salvar(p *Pedido) error {
f.pedidosSalvos = append(f.pedidosSalvos, p)
return nil
}
// Spy: registra chamadas de e-mail
type SpyNotificador struct {
totalEnvios int
ultimoEmail string
}
func (s *SpyNotificador) EnviarEmail(destinatario, msg string) error {
s.totalEnvios++
s.ultimoEmail = destinatario
return nil
}
Vantagens dos Dublês Manuais:
- Auxílio total do compilador: Se você alterar a assinatura do método na interface, o compilador do Go aponta instantaneamente onde o seu fake/stub quebrou. Mocks baseados em strings de nomes de métodos (
"Cobrar") só falham em tempo de execução. - Velocidade de compilação imbatível: Zero reflexão e zero overhead de bibliotecas pesadas.
- Depuração transparente: Você pode colocar
fmt.Printlnou usar um breakpoint diretamente dentro do seu fake.
Parte 3: Mocks Rigorosos com testify/mock
Quando você precisa validar protocolos estritos de interação (por exemplo: garantir que a cobrança no cartão ocorra exatamente uma única vez, ou que o serviço de notificação jamais seja acionado se o pagamento for reprovado), o pacote github.com/stretchr/testify/mock é o padrão da indústria.
3. Criando a Struct de Mock
Para criar um mock com Testify:
- Incorpore a struct
mock.Mock. - Em cada método da interface, intercepte os argumentos usando
m.Called(args...). - Retorne os valores tipados extraídos do objeto
mock.Arguments.
package mocks
import (
"github.com/stretchr/testify/mock"
)
type GatewayMock struct {
mock.Mock
}
func (m *GatewayMock) Cobrar(pedidoID string, valor float64) (bool, error) {
// Intercepta a chamada
args := m.Called(pedidoID, valor)
// Retorna os valores pré-configurados
return args.Bool(0), args.Error(1)
}
Métodos de extração de tipos do mock.Arguments:
args.Bool(i): para retornos booleanos.args.String(i): para strings.args.Int(i): para inteiros.args.Error(i): para o retorno do tipoerror.args.Get(i).(*MeuTipo): para structs, interfaces ou ponteiros concretos através de type assertion.
4. Programando Expectativas com m.On()
No corpo do seu teste, você configura exatamente como o mock deve se comportar antes de disparar a lógica de negócio:
mockGw := new(GatewayMock)
// 1. Chamada estrita com parâmetros exatos
mockGw.On("Cobrar", "ped-100", 250.0).Return(true, nil).Once()
// 2. Chamada flexível usando casadores (Matchers)
mockGw.On("Cobrar", mock.Anything, mock.Anything).Return(false, ErrSaldoInsuficiente)
// 3. Casador baseado em predicado lógico (MatchedBy)
mockGw.On("Salvar", mock.MatchedBy(func(p *Pedido) bool {
return p.Valor > 0 && p.Status == "PAGO"
})).Return(nil)
5. Modificadores de Frequência de Chamadas:
.Once(): a chamada deve ocorrer exatamente uma vez..Twice(): a chamada deve ocorrer exatamente duas vezes..Times(n): a chamada deve ocorrer exatamente $n$ vezes..Maybe(): a chamada pode ou não acontecer (não falha o teste se não for invocada).
6. Verificando o Cumprimento das Expectativas
No final do teste, você deve validar se todas as expectativas foram satisfeitas:
// Falha o teste se algum método configurado no On() não tiver sido chamado
mockGw.AssertExpectations(t)
// Validações pontuais adicionais:
mockGw.AssertCalled(t, "Cobrar", "ped-100", 250.0)
mockEmail.AssertNotCalled(t, "EnviarEmail", mock.Anything, mock.Anything)
Parte 4: Geração Automática com mockgen
Quando o projeto possui interfaces com muitos métodos ou integrações de terceiros cujo contrato é extenso, escrever mocks na mão pode se tornar tedioso. A ferramenta oficial da comunidade é o mockgen (mantido pela Uber em go.uber.org/mock/mockgen).
Como usar o mockgen:
- Instale o binário:
go install go.uber.org/mock/mockgen@latest - Adicione uma diretiva de geração no topo do arquivo que contém a interface:
//go:generate mockgen -source=repositorio.go -destination=mocks/mock_repositorio.go -package=mocks package pedido type Repositorio interface { Buscar(id string) (*Pedido, error) Salvar(p *Pedido) error } - Execute no terminal:
go generate ./...
O mockgen criará o arquivo mocks/mock_repositorio.go com mocks totalmente tipados e integrados ao framework gomock.
Parte 5: Código Prático Completo de Produção
Abaixo está o exemplo completo de um processador de pagamentos de e-commerce e sua suíte de testes unitários isolada com dublês manuais e testify/mock.
O Código de Domínio (checkout.go):
package checkout
import (
"errors"
"fmt"
)
var (
ErrPedidoInvalido = errors.New("pedido com dados inválidos")
ErrPagamentoRecusado = errors.New("pagamento recusado pela operadora")
)
type Pedido struct {
ID string
Valor float64
ClienteEmail string
Status string
}
// 1. Interfaces pequenas e explícitas no ponto de consumo
type GatewayPagamento interface {
Cobrar(pedidoID string, valor float64) (bool, error)
}
type PedidoRepository interface {
Salvar(p *Pedido) error
}
type Notificador interface {
EnviarEmail(destinatario, assunto, corpo string) error
}
// 2. Serviço com dependências injetadas no construtor
type ProcessadorCheckout struct {
gateway GatewayPagamento
repo PedidoRepository
email Notificador
}
func NovoProcessadorCheckout(g GatewayPagamento, r PedidoRepository, e Notificador) *ProcessadorCheckout {
return &ProcessadorCheckout{
gateway: g,
repo: r,
email: e,
}
}
func (p *ProcessadorCheckout) Processar(pedido *Pedido) error {
if pedido == nil || pedido.Valor <= 0 || pedido.ClienteEmail == "" {
return ErrPedidoInvalido
}
sucesso, err := p.gateway.Cobrar(pedido.ID, pedido.Valor)
if err != nil {
pedido.Status = "FALHA_PROCESSAMENTO"
_ = p.repo.Salvar(pedido)
return fmt.Errorf("falha no gateway: %w", err)
}
if !sucesso {
pedido.Status = "RECUSADO"
_ = p.repo.Salvar(pedido)
return ErrPagamentoRecusado
}
pedido.Status = "PAGO"
if err := p.repo.Salvar(pedido); err != nil {
return fmt.Errorf("falha ao salvar pedido aprovado: %w", err)
}
// Regra de negócio: Notifica apenas se o pagamento foi concluído com sucesso
_ = p.email.EnviarEmail(pedido.ClienteEmail, "Pedido Aprovado", "Seu pedido foi confirmado!")
return nil
}
A Suíte de Testes com Dublês e Mocks (checkout_test.go):
package checkout_test
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"meu-modulo/checkout"
)
// ==========================================
// 1. DUBLÊS MANUAIS (GO PURO - FAKE & SPY)
// ==========================================
type FakeRepo struct {
pedidosSalvos []*checkout.Pedido
}
func (f *FakeRepo) Salvar(p *checkout.Pedido) error {
f.pedidosSalvos = append(f.pedidosSalvos, p)
return nil
}
type SpyEmail struct {
chamadas int
ultimoDestinatario string
}
func (s *SpyEmail) EnviarEmail(destinatario, assunto, corpo string) error {
s.chamadas++
s.ultimoDestinatario = destinatario
return nil
}
type StubGateway struct {
sucesso bool
err error
}
func (g *StubGateway) Cobrar(pedidoID string, valor float64) (bool, error) {
return g.sucesso, g.err
}
func TestCheckout_ComDublesManuais(t *testing.T) {
t.Parallel()
t.Run("sucesso: aprova pedido, salva no banco e notifica cliente", func(t *testing.T) {
repo := &FakeRepo{}
emailSpy := &SpyEmail{}
gatewayStub := &StubGateway{sucesso: true, err: nil}
proc := checkout.NovoProcessadorCheckout(gatewayStub, repo, emailSpy)
pedido := &checkout.Pedido{
ID: "ped-123",
Valor: 250.0,
ClienteEmail: "joao@email.com",
}
err := proc.Processar(pedido)
require.NoError(t, err)
// Valida alterações de estado
assert.Equal(t, "PAGO", pedido.Status)
require.Len(t, repo.pedidosSalvos, 1)
assert.Equal(t, "ped-123", repo.pedidosSalvos[0].ID)
// Valida efeitos colaterais via Spy
assert.Equal(t, 1, emailSpy.chamadas)
assert.Equal(t, "joao@email.com", emailSpy.ultimoDestinatario)
})
t.Run("recusado: atualiza status no banco mas NÃO envia email", func(t *testing.T) {
repo := &FakeRepo{}
emailSpy := &SpyEmail{}
gatewayStub := &StubGateway{sucesso: false, err: nil}
proc := checkout.NovoProcessadorCheckout(gatewayStub, repo, emailSpy)
pedido := &checkout.Pedido{
ID: "ped-456",
Valor: 100.0,
ClienteEmail: "maria@email.com",
}
err := proc.Processar(pedido)
require.ErrorIs(t, err, checkout.ErrPagamentoRecusado)
assert.Equal(t, "RECUSADO", pedido.Status)
assert.Equal(t, 0, emailSpy.chamadas, "e-mail jamais deve ser enviado quando pagamento é recusado")
})
}
// ==========================================
// 2. MOCKS RIGOROSOS COM TESTIFY/MOCK
// ==========================================
type MockGateway struct {
mock.Mock
}
func (m *MockGateway) Cobrar(pedidoID string, valor float64) (bool, error) {
args := m.Called(pedidoID, valor)
return args.Bool(0), args.Error(1)
}
type MockRepository struct {
mock.Mock
}
func (m *MockRepository) Salvar(p *checkout.Pedido) error {
args := m.Called(p)
return args.Error(0)
}
type MockEmail struct {
mock.Mock
}
func (m *MockEmail) EnviarEmail(destinatario, assunto, corpo string) error {
args := m.Called(destinatario, assunto, corpo)
return args.Error(0)
}
func TestCheckout_ComTestifyMock(t *testing.T) {
t.Parallel()
t.Run("falha de conexão no gateway deve registrar status e não disparar e-mail", func(t *testing.T) {
mockGw := new(MockGateway)
mockRepo := new(MockRepository)
mockMail := new(MockEmail)
errGateway := errors.New("timeout na conexão com adquirente")
// 1. O Gateway DEVE ser chamado 1 vez com os argumentos especificados
mockGw.On("Cobrar", "ped-999", 500.0).Return(false, errGateway).Once()
// 2. O Repositório DEVE ser chamado salvando com status "FALHA_PROCESSAMENTO"
mockRepo.On("Salvar", mock.MatchedBy(func(p *checkout.Pedido) bool {
return p.ID == "ped-999" && p.Status == "FALHA_PROCESSAMENTO"
})).Return(nil).Once()
// 3. Nenhuma expectativa é configurada para mockMail (se for chamado, o teste quebra!)
proc := checkout.NovoProcessadorCheckout(mockGw, mockRepo, mockMail)
pedido := &checkout.Pedido{
ID: "ped-999",
Valor: 500.0,
ClienteEmail: "cliente@email.com",
}
err := proc.Processar(pedido)
require.Error(t, err)
assert.Contains(t, err.Error(), "timeout na conexão")
// Validação estrita de contratos
mockGw.AssertExpectations(t)
mockRepo.AssertExpectations(t)
mockMail.AssertNotCalled(t, "EnviarEmail", mock.Anything, mock.Anything, mock.Anything)
})
}
Parte 6: Checklist de Qualidade e Armadilhas Comuns
- A armadilha do Over-Mocking: Não tente mockar tudo o que vê pela frente. Se você estiver testando uma função utilitária que calcula impostos ou formata texto, não crie interface nem mock. Teste o algoritmo real diretamente. Mocks devem ser reservados para I/O, rede, banco e serviços lentos ou estocásticos.
- Não mocke tipos concretos que você não controla: Nunca tente criar interfaces espelhando diretamente structs de terceiros (como a struct
s3.Clientda AWS). Crie um adaptador próprio no seu projeto (ex:type Armazenamento interface { Upload(nome string, dados io.Reader) error }) e mocke o seu próprio adaptador. - Esquecer
m.AssertExpectations(t): Se você definir expectativas com.On()e esquecer de invocarm.AssertExpectations(t)no final do teste, o Testify não acusará erro caso o método nunca tenha sido chamado pelo seu código! - Preferir Dublês Manuais para casos simples: Se uma struct anônima com um slice em memória resolve seu teste, prefira-a em vez de configurar strings mágicas de mock. O refactoring automático da IDE e a checagem do compilador agradecerão.
Exercício Prático Proposto
- Modele um serviço de autenticação
ServicoAuthque receba duas interfaces:HashValidador(Comparar(senha, hash string) bool) eTokenGerador(GerarToken(usuarioID string) (string, error)). - Escreva testes usando
testify/mock:- Um teste para senha incorreta garantindo que
TokenGeradornunca seja invocado (AssertNotCalled). - Um teste para senha correta garantindo que o token seja retornado com sucesso.
- Um teste para senha incorreta garantindo que
- Execute com
-racee valide a ausência de condições de corrida.