<h2>Por que Testamos com Mocks?</h2>
<p>Testes unitários são o alicerce de um código confiável e manutenível. No entanto, nem sempre conseguimos testar nossas funções isoladamente — às vezes, elas dependem de bancos de dados, APIs externas, sistemas de arquivo ou outros serviços que não podemos controlar completamente durante os testes. Aqui entra o conceito de <strong>mocks</strong>.</p>
<p>Um mock é uma réplica controlável de um objeto real. Ele simula o comportamento de uma dependência externa, permitindo que você teste apenas a lógica da sua função sem efeitos colaterais. Em Go, mocks são construídos sobre um pilar fundamental da linguagem: <strong>interfaces</strong>. Sem interfaces, não haveria um caminho claro para substituir implementações reais por mocks. É por isso que todo desenvolvedor Go que trabalha com testes precisa dominar esse trio: interfaces, testify/mock e mockery.</p>
<h2>Interfaces: A Fundação dos Mocks em Go</h2>
<h3>O que é uma Interface em Go</h3>
<p>Uma interface em Go é um contrato que define um conjunto de métodos. Qualquer tipo que implemente todos esses métodos satisfaz implicitamente a interface, sem precisar de uma declaração explícita de herança (como em linguagens orientadas a objetos tradicionais). Essa característica torna Go extremamente flexível para testes.</p>
<pre><code class="language-go">package payment
// PaymentGateway define o contrato para um processador de pagamentos
type PaymentGateway interface {
Charge(amount float64) (transactionID string, err error)
Refund(transactionID string) error
}
// OrderService depende de um PaymentGateway, não de uma implementação concreta
type OrderService struct {
gateway PaymentGateway
}
func NewOrderService(gateway PaymentGateway) *OrderService {
return &OrderService{gateway: gateway}
}
func (os *OrderService) PlaceOrder(amount float64) (string, error) {
return os.gateway.Charge(amount)
}</code></pre>
<p>Neste exemplo, <code>OrderService</code> não depende de uma implementação específica de pagamento (como Stripe ou PayPal). Ele trabalha com qualquer coisa que implemente a interface <code>PaymentGateway</code>. Isso é ouro puro para testes: você pode facilmente passar um mock que implementa essa interface.</p>
<h3>Criando um Mock Manual</h3>
<p>Antes de usar ferramentas automatizadas, é importante entender como um mock é construído manualmente. Um mock é simplesmente uma struct que implementa a interface e armazena informações sobre as chamadas recebidas.</p>
<pre><code class="language-go">package payment
// MockPaymentGateway é um mock manual de PaymentGateway
type MockPaymentGateway struct {
ChargeCalls []float64
ChargeReturns struct {
TransactionID string
Err error
}
RefundCalls []string
RefundErr error
}
func (m *MockPaymentGateway) Charge(amount float64) (transactionID string, err error) {
m.ChargeCalls = append(m.ChargeCalls, amount)
return m.ChargeReturns.TransactionID, m.ChargeReturns.Err
}
func (m *MockPaymentGateway) Refund(transactionID string) error {
m.RefundCalls = append(m.RefundCalls, transactionID)
return m.RefundErr
}</code></pre>
<p>Mocks manuais funcionam, mas ficam verbosos rapidamente. Para cada interface com múltiplos métodos, você precisa escrever bastante código boilerplate. É aqui que as ferramentas de teste entram em cena.</p>
<h2>testify/mock: Automação Inteligente de Mocks</h2>
<h3>Introdução ao testify/mock</h3>
<p>O pacote <code>testify/mock</code> é a escolha padrão da comunidade Go para criar mocks programáticos. Em vez de escrever structs manuais, você constrói mocks dinâmicos usando uma API fluente. Além disso, <code>testify</code> oferece funções de assertion que tornam os testes mais legíveis.</p>
<pre><code class="language-go">package payment
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestOrderServiceWithTestifyMock(t *testing.T) {
// Criar um mock dinâmico
mockGateway := new(mock.Mock)
// Configurar expectativas: quando Charge for chamado com 100.0,
// retorne ("txn123", nil)
mockGateway.On("Charge", 100.0).Return("txn123", nil)
// Criar a struct que encapsula o mock
gateway := &TestPaymentGateway{Mock: mockGateway}
service := NewOrderService(gateway)
// Executar a função
txnID, err := service.PlaceOrder(100.0)
// Verificar o resultado
assert.NoError(t, err)
assert.Equal(t, "txn123", txnID)
// Verificar que as expectativas foram atendidas
mockGateway.AssertExpectations(t)
}
// TestPaymentGateway encapsula o mock.Mock e implementa a interface
type TestPaymentGateway struct {
mock.Mock
}
func (m *TestPaymentGateway) Charge(amount float64) (string, error) {
args := m.Called(amount)
return args.String(0), args.Error(1)
}
func (m *TestPaymentGateway) Refund(transactionID string) error {
args := m.Called(transactionID)
return args.Error(0)
}</code></pre>
<p>O <code>testify/mock</code> oferece uma sintaxe clara com <code>On()</code>, <code>Return()</code> e <code>Called()</code>. Você especifica o que espera que aconteça, executa o código e verifica se as expectativas foram atendidas. É funcional e direto.</p>
<h3>Validando Chamadas e Argumentos</h3>
<p>Além de verificar retornos, <code>testify/mock</code> permite validar exatamente como seus mocks foram chamados. Isso é crucial para testes de comportamento.</p>
<pre><code class="language-go">func TestRefundIsCalledWithCorrectID(t *testing.T) {
mockGateway := new(mock.Mock)
mockGateway.On("Charge", mock.MatchedBy(func(amount float64) bool {
return amount > 0
})).Return("txn456", nil)
mockGateway.On("Refund", "txn456").Return(nil)
gateway := &TestPaymentGateway{Mock: mockGateway}
service := NewOrderService(gateway)
txnID, _ := service.PlaceOrder(50.0)
service.RefundOrder(txnID)
// Verificar que Refund foi chamado exatamente uma vez com "txn456"
mockGateway.AssertCalled(t, "Refund", "txn456")
mockGateway.AssertNumberOfCalls(t, "Charge", 1)
}</code></pre>
<p>Com <code>MatchedBy()</code>, você pode criar validações customizadas para argumentos. Com <code>AssertCalled()</code> e <code>AssertNumberOfCalls()</code>, você verifica o histórico de chamadas. Isso oferece segurança e clareza sobre o comportamento testado.</p>
<h2>mockery: Geração Automática de Mocks</h2>
<h3>Por que Usar mockery</h3>
<p>À medida que seus projetos crescem com centenas de interfaces, escrever a struct wrapper manualmente (como <code>TestPaymentGateway</code> acima) fica entediante. O <code>mockery</code> é uma ferramenta que <strong>gera automaticamente</strong> mocks baseados em interfaces Go usando análise de código. Você define a interface, e o mockery cria toda a boilerplate para você.</p>
<p>Instale mockery localmente no seu projeto:</p>
<pre><code class="language-bash">go install github.com/vektra/mockery/v2@latest</code></pre>
<h3>Gerando Mocks com mockery</h3>
<p>Considere uma interface em um arquivo <code>gateway.go</code>:</p>
<pre><code class="language-go">package payment
//go:generate mockery --name=PaymentGateway
type PaymentGateway interface {
Charge(amount float64) (transactionID string, err error)
Refund(transactionID string) error
}</code></pre>
<p>Quando você executar <code>go generate ./...</code>, mockery analisa a interface e gera um mock automático em <code>mocks/MockPaymentGateway.go</code>. Você não escreve nada — a ferramenta faz tudo.</p>
<pre><code class="language-bash">go generate ./...</code></pre>
<h3>Usando Mocks Gerados</h3>
<p>Os mocks gerados pelo mockery já vêm com suporte a testify/mock integrado:</p>
<pre><code class="language-go">package payment
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"seu-modulo/mocks"
)
func TestWithGeneratedMock(t *testing.T) {
// mockery gera uma struct que já implementa a interface
mockGateway := mocks.NewMockPaymentGateway(t)
// Configurar expectativas com a mesma API do testify/mock
mockGateway.On("Charge", mock.MatchedBy(func(amount float64) bool {
return amount == 75.5
})).Return("txn789", nil)
mockGateway.On("Refund", "txn789").Return(nil)
service := NewOrderService(mockGateway)
// Executar
txnID, err := service.PlaceOrder(75.5)
assert.NoError(t, err)
assert.Equal(t, "txn789", txnID)
// Verificar expectativas
mockGateway.AssertExpectations(t)
}</code></pre>
<p>A beleza do mockery é que ele gera a estrutura padrão que você precisa. O mock gerado já sabe como converter argumentos, gerenciar retornos e trabalhar com <code>testify/mock</code>. Você economiza dezenas de linhas de código repetitivo.</p>
<h3>Configurando mockery para seu Projeto</h3>
<p>Para projetos maiores, crie um arquivo <code>go.generate.go</code> na raiz ou em um diretório:</p>
<pre><code class="language-go">//go:generate mockery --all --output=mocks --outpkg=mocks
package payment</code></pre>
<p>Isso gera mocks para <strong>todas</strong> as interfaces do pacote no diretório <code>mocks/</code>. O flag <code>--output</code> define onde os mocks serão salvos e <code>--outpkg</code> define o pacote deles.</p>
<p>Para interfaces específicas, use <code>--name</code>:</p>
<pre><code class="language-go">//go:generate mockery --name=PaymentGateway --name=Logger --output=mocks --outpkg=mocks</code></pre>
<h2>Exemplo Prático Completo: Sistema de Pedidos</h2>
<p>Agora vamos juntar tudo em um exemplo real. Considere um sistema onde você precisa processar pedidos, logar eventos e registrar auditoria:</p>
<pre><code class="language-go">package order
import "context"
type PaymentGateway interface {
Charge(ctx context.Context, amount float64) (transactionID string, err error)
Refund(ctx context.Context, transactionID string) error
}
type AuditLogger interface {
LogEvent(ctx context.Context, event string, data map[string]interface{}) error
}
type OrderProcessor struct {
payment PaymentGateway
audit AuditLogger
}
func NewOrderProcessor(payment PaymentGateway, audit AuditLogger) *OrderProcessor {
return &OrderProcessor{
payment: payment,
audit: audit,
}
}
func (op *OrderProcessor) ProcessOrder(ctx context.Context, orderID string, amount float64) error {
// Registar tentativa
op.audit.LogEvent(ctx, "order_processing_started", map[string]interface{}{
"order_id": orderID,
"amount": amount,
})
// Processar pagamento
transactionID, err := op.payment.Charge(ctx, amount)
if err != nil {
op.audit.LogEvent(ctx, "payment_failed", map[string]interface{}{
"order_id": orderID,
"error": err.Error(),
})
return err
}
// Registar sucesso
op.audit.LogEvent(ctx, "order_completed", map[string]interface{}{
"order_id": orderID,
"transaction_id": transactionID,
})
return nil
}</code></pre>
<p>Agora, com mockery, gere os mocks:</p>
<pre><code class="language-go">//go:generate mockery --all --output=mocks --outpkg=mocks
package order</code></pre>
<p>Execute:</p>
<pre><code class="language-bash">go generate ./...</code></pre>
<p>E escreva seus testes:</p>
<pre><code class="language-go">package order
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"seu-modulo/mocks"
)
func TestProcessOrderSuccess(t *testing.T) {
ctx := context.Background()
// Criar mocks
mockPayment := mocks.NewMockPaymentGateway(t)
mockAudit := mocks.NewMockAuditLogger(t)
// Configurar expectativas
mockPayment.On("Charge", ctx, 100.0).Return("txn123", nil)
mockAudit.On("LogEvent", ctx, "order_processing_started", mock.MatchedBy(func(data map[string]interface{}) bool {
return data["order_id"] == "order1" && data["amount"] == 100.0
})).Return(nil)
mockAudit.On("LogEvent", ctx, "order_completed", mock.MatchedBy(func(data map[string]interface{}) bool {
return data["transaction_id"] == "txn123"
})).Return(nil)
processor := NewOrderProcessor(mockPayment, mockAudit)
err := processor.ProcessOrder(ctx, "order1", 100.0)
assert.NoError(t, err)
mockPayment.AssertExpectations(t)
mockAudit.AssertExpectations(t)
}
func TestProcessOrderPaymentFailure(t *testing.T) {
ctx := context.Background()
mockPayment := mocks.NewMockPaymentGateway(t)
mockAudit := mocks.NewMockAuditLogger(t)
paymentErr := errors.New("insufficient funds")
mockPayment.On("Charge", ctx, 500.0).Return("", paymentErr)
mockAudit.On("LogEvent", ctx, "order_processing_started", mock.Anything).Return(nil)
mockAudit.On("LogEvent", ctx, "payment_failed", mock.MatchedBy(func(data map[string]interface{}) bool {
return data["error"] == paymentErr.Error()
})).Return(nil)
processor := NewOrderProcessor(mockPayment, mockAudit)
err := processor.ProcessOrder(ctx, "order2", 500.0)
assert.Error(t, err)
assert.Equal(t, paymentErr, err)
mockPayment.AssertExpectations(t)
mockAudit.AssertExpectations(t)
}</code></pre>
<p>Este exemplo demonstra um fluxo realista: você tem múltiplas dependências (pagamento e auditoria), precisa testar o comportamento quando tudo funciona e quando há falhas. Mockery simplifica a criação de mocks para ambas as interfaces, e testify/mock valida que o comportamento esperado ocorreu.</p>
<h2>Conclusão</h2>
<p>Aprendemos três conceitos complementares que formam a base de testes robustos em Go: <strong>interfaces</strong> como fundação de substituibilidade (você não testa implementações concretas, testa contratos), <strong>testify/mock</strong> como ferramenta elegante para criar expectativas e validar comportamento em tempo de execução, e <strong>mockery</strong> como solução de automação que elimina boilerplate. A progressão é natural: comece entendendo interfaces, use testify/mock para ganhar confiança nas asserções, e migre para mockery quando seus testes ganharem escala. Um desenvolvedor que domina esses três pilares escreve testes de qualidade, manutenção fácil e cobertura confiável.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://pkg.go.dev/github.com/stretchr/testify/mock" target="_blank" rel="noopener noreferrer">Package mock - testify/mock</a></li>
<li><a href="https://github.com/vektra/mockery" target="_blank" rel="noopener noreferrer">Mockery - Automated Mock Code Generation Tool</a></li>
<li><a href="https://golang.org/doc/effective_go#interfaces" target="_blank" rel="noopener noreferrer">Effective Go - Interfaces</a></li>
<li><a href="https://gobyexample.com/interfaces" target="_blank" rel="noopener noreferrer">Go by Example - Interfaces</a></li>
<li><a href="https://golang.org/blog/using-go-modules" target="_blank" rel="noopener noreferrer">Testing in Go - Andrew Gerrand</a></li>
</ul>
<p><!-- FIM --></p>