Go

O que Todo Dev Deve Saber sobre Mocks em Go: Interfaces, testify/mock e mockery

14 min de leitura

O que Todo Dev Deve Saber sobre Mocks em Go: Interfaces, testify/mock e mockery

Por que Testamos com Mocks? 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 mocks. 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: interfaces. 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. Interfaces: A Fundação dos Mocks em Go O que é uma Interface em Go Uma interface em Go é um contrato que define um conjunto de métodos. Qualquer tipo

<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 &amp;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 (

&quot;testing&quot;

&quot;github.com/stretchr/testify/assert&quot;

&quot;github.com/stretchr/testify/mock&quot;

)

func TestOrderServiceWithTestifyMock(t *testing.T) {

// Criar um mock dinâmico

mockGateway := new(mock.Mock)

// Configurar expectativas: quando Charge for chamado com 100.0,

// retorne (&quot;txn123&quot;, nil)

mockGateway.On(&quot;Charge&quot;, 100.0).Return(&quot;txn123&quot;, nil)

// Criar a struct que encapsula o mock

gateway := &amp;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, &quot;txn123&quot;, 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(&quot;Charge&quot;, mock.MatchedBy(func(amount float64) bool {

return amount &gt; 0

})).Return(&quot;txn456&quot;, nil)

mockGateway.On(&quot;Refund&quot;, &quot;txn456&quot;).Return(nil)

gateway := &amp;TestPaymentGateway{Mock: mockGateway}

service := NewOrderService(gateway)

txnID, _ := service.PlaceOrder(50.0)

service.RefundOrder(txnID)

// Verificar que Refund foi chamado exatamente uma vez com &quot;txn456&quot;

mockGateway.AssertCalled(t, &quot;Refund&quot;, &quot;txn456&quot;)

mockGateway.AssertNumberOfCalls(t, &quot;Charge&quot;, 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 (

&quot;testing&quot;

&quot;github.com/stretchr/testify/assert&quot;

&quot;github.com/stretchr/testify/mock&quot;

&quot;seu-modulo/mocks&quot;

)

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(&quot;Charge&quot;, mock.MatchedBy(func(amount float64) bool {

return amount == 75.5

})).Return(&quot;txn789&quot;, nil)

mockGateway.On(&quot;Refund&quot;, &quot;txn789&quot;).Return(nil)

service := NewOrderService(mockGateway)

// Executar

txnID, err := service.PlaceOrder(75.5)

assert.NoError(t, err)

assert.Equal(t, &quot;txn789&quot;, 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 &quot;context&quot;

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 &amp;OrderProcessor{

payment: payment,

audit: audit,

}

}

func (op *OrderProcessor) ProcessOrder(ctx context.Context, orderID string, amount float64) error {

// Registar tentativa

op.audit.LogEvent(ctx, &quot;order_processing_started&quot;, map[string]interface{}{

&quot;order_id&quot;: orderID,

&quot;amount&quot;: amount,

})

// Processar pagamento

transactionID, err := op.payment.Charge(ctx, amount)

if err != nil {

op.audit.LogEvent(ctx, &quot;payment_failed&quot;, map[string]interface{}{

&quot;order_id&quot;: orderID,

&quot;error&quot;: err.Error(),

})

return err

}

// Registar sucesso

op.audit.LogEvent(ctx, &quot;order_completed&quot;, map[string]interface{}{

&quot;order_id&quot;: orderID,

&quot;transaction_id&quot;: 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 (

&quot;context&quot;

&quot;errors&quot;

&quot;testing&quot;

&quot;github.com/stretchr/testify/assert&quot;

&quot;github.com/stretchr/testify/mock&quot;

&quot;seu-modulo/mocks&quot;

)

func TestProcessOrderSuccess(t *testing.T) {

ctx := context.Background()

// Criar mocks

mockPayment := mocks.NewMockPaymentGateway(t)

mockAudit := mocks.NewMockAuditLogger(t)

// Configurar expectativas

mockPayment.On(&quot;Charge&quot;, ctx, 100.0).Return(&quot;txn123&quot;, nil)

mockAudit.On(&quot;LogEvent&quot;, ctx, &quot;order_processing_started&quot;, mock.MatchedBy(func(data map[string]interface{}) bool {

return data[&quot;order_id&quot;] == &quot;order1&quot; &amp;&amp; data[&quot;amount&quot;] == 100.0

})).Return(nil)

mockAudit.On(&quot;LogEvent&quot;, ctx, &quot;order_completed&quot;, mock.MatchedBy(func(data map[string]interface{}) bool {

return data[&quot;transaction_id&quot;] == &quot;txn123&quot;

})).Return(nil)

processor := NewOrderProcessor(mockPayment, mockAudit)

err := processor.ProcessOrder(ctx, &quot;order1&quot;, 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(&quot;insufficient funds&quot;)

mockPayment.On(&quot;Charge&quot;, ctx, 500.0).Return(&quot;&quot;, paymentErr)

mockAudit.On(&quot;LogEvent&quot;, ctx, &quot;order_processing_started&quot;, mock.Anything).Return(nil)

mockAudit.On(&quot;LogEvent&quot;, ctx, &quot;payment_failed&quot;, mock.MatchedBy(func(data map[string]interface{}) bool {

return data[&quot;error&quot;] == paymentErr.Error()

})).Return(nil)

processor := NewOrderProcessor(mockPayment, mockAudit)

err := processor.ProcessOrder(ctx, &quot;order2&quot;, 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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

O que Todo Dev Deve Saber sobre Pacotes e Módulos em Go: go mod, imports e Organização de Projetos
O que Todo Dev Deve Saber sobre Pacotes e Módulos em Go: go mod, imports e Organização de Projetos

Entendendo Módulos em Go Um módulo em Go é uma coleção de pacotes Go armazena...

Guia Completo de Testes de Integração em Go: Banco Real com testcontainers-go
Guia Completo de Testes de Integração em Go: Banco Real com testcontainers-go

Por que Testes de Integração Importam em Go Testes de integração são diferent...

Autenticação JWT em APIs Go: Geração, Validação e Refresh Tokens: Do Básico ao Avançado
Autenticação JWT em APIs Go: Geração, Validação e Refresh Tokens: Do Básico ao Avançado

Entendendo JWT: Fundamentos e Estrutura JSON Web Token (JWT) é um padrão aber...