Go Admin

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

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

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 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.

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)
}

Neste exemplo, OrderService não depende de uma implementação específica de pagamento (como Stripe ou PayPal). Ele trabalha com qualquer coisa que implemente a interface PaymentGateway. Isso é ouro puro para testes: você pode facilmente passar um mock que implementa essa interface.

Criando um Mock Manual

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.

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
}

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.

testify/mock: Automação Inteligente de Mocks

Introdução ao testify/mock

O pacote testify/mock é 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, testify oferece funções de assertion que tornam os testes mais legíveis.

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)
}

O testify/mock oferece uma sintaxe clara com On(), Return() e Called(). Você especifica o que espera que aconteça, executa o código e verifica se as expectativas foram atendidas. É funcional e direto.

Validando Chamadas e Argumentos

Além de verificar retornos, testify/mock permite validar exatamente como seus mocks foram chamados. Isso é crucial para testes de comportamento.

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)
}

Com MatchedBy(), você pode criar validações customizadas para argumentos. Com AssertCalled() e AssertNumberOfCalls(), você verifica o histórico de chamadas. Isso oferece segurança e clareza sobre o comportamento testado.

mockery: Geração Automática de Mocks

Por que Usar mockery

À medida que seus projetos crescem com centenas de interfaces, escrever a struct wrapper manualmente (como TestPaymentGateway acima) fica entediante. O mockery é uma ferramenta que gera automaticamente mocks baseados em interfaces Go usando análise de código. Você define a interface, e o mockery cria toda a boilerplate para você.

Instale mockery localmente no seu projeto:

go install github.com/vektra/mockery/v2@latest

Gerando Mocks com mockery

Considere uma interface em um arquivo gateway.go:

package payment

//go:generate mockery --name=PaymentGateway
type PaymentGateway interface {
    Charge(amount float64) (transactionID string, err error)
    Refund(transactionID string) error
}

Quando você executar go generate ./..., mockery analisa a interface e gera um mock automático em mocks/MockPaymentGateway.go. Você não escreve nada — a ferramenta faz tudo.

go generate ./...

Usando Mocks Gerados

Os mocks gerados pelo mockery já vêm com suporte a testify/mock integrado:

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)
}

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 testify/mock. Você economiza dezenas de linhas de código repetitivo.

Configurando mockery para seu Projeto

Para projetos maiores, crie um arquivo go.generate.go na raiz ou em um diretório:

//go:generate mockery --all --output=mocks --outpkg=mocks

package payment

Isso gera mocks para todas as interfaces do pacote no diretório mocks/. O flag --output define onde os mocks serão salvos e --outpkg define o pacote deles.

Para interfaces específicas, use --name:

//go:generate mockery --name=PaymentGateway --name=Logger --output=mocks --outpkg=mocks

Exemplo Prático Completo: Sistema de Pedidos

Agora vamos juntar tudo em um exemplo real. Considere um sistema onde você precisa processar pedidos, logar eventos e registrar auditoria:

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
}

Agora, com mockery, gere os mocks:

//go:generate mockery --all --output=mocks --outpkg=mocks

package order

Execute:

go generate ./...

E escreva seus testes:

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)
}

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.

Conclusão

Aprendemos três conceitos complementares que formam a base de testes robustos em Go: interfaces como fundação de substituibilidade (você não testa implementações concretas, testa contratos), testify/mock como ferramenta elegante para criar expectativas e validar comportamento em tempo de execução, e mockery 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.

Referências


Artigos relacionados