Pular para o conteúdo

Trilha de Go · parte 14 de 37

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

Tamanho da fonte:
Largura da coluna:

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

ParteSeçãoO que você vai dominar
1. Filosofia1 e 2Interfaces no ponto de consumo e a taxonomia oficial dos 5 tipos de dublês de teste
2. Dublês Manuais3 e 4Fakes em memória, Stubs de erro e Spies com contadores usando apenas Go puro
3. Testify Mock5 a 7Struct embedding de mock.Mock, configuração com On(), casadores flexíveis e AssertExpectations
4. Geração Automática8Quando e como automatizar a criação de mocks com mockgen e diretivas //go:generate
5. Código de Produção9 e 10Caso 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:

taxonomia duble teste
figura 1 · taxonomia duble teste
  1. Dummy: Passado apenas para satisfazer a assinatura de parâmetros obrigatórios, mas nunca é acessado ou invocado pela lógica testada (ex: nil ou uma struct vazia).
  2. Stub: Fornece respostas enlatadas e pré-programadas para chamadas específicas (ex: sempre retorna (50.0, nil) ou sempre retorna ErrConexao).
  3. 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]*Pedido em memória em vez de um banco SQL real).
  4. 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.
  5. 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.Println ou 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.

fluxo testify mock
figura 2 · fluxo testify mock

3. Criando a Struct de Mock

Para criar um mock com Testify:

  1. Incorpore a struct mock.Mock.
  2. Em cada método da interface, intercepte os argumentos usando m.Called(args...).
  3. 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 tipo error.
  • 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:

  1. Instale o binário:
    go install go.uber.org/mock/mockgen@latest
  2. 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
    }
  3. 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

  1. 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.
  2. Não mocke tipos concretos que você não controla: Nunca tente criar interfaces espelhando diretamente structs de terceiros (como a struct s3.Client da 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.
  3. Esquecer m.AssertExpectations(t): Se você definir expectativas com .On() e esquecer de invocar m.AssertExpectations(t) no final do teste, o Testify não acusará erro caso o método nunca tenha sido chamado pelo seu código!
  4. 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

  1. Modele um serviço de autenticação ServicoAuth que receba duas interfaces: HashValidador (Comparar(senha, hash string) bool) e TokenGerador (GerarToken(usuarioID string) (string, error)).
  2. Escreva testes usando testify/mock:
    • Um teste para senha incorreta garantindo que TokenGerador nunca seja invocado (AssertNotCalled).
    • Um teste para senha correta garantindo que o token seja retornado com sucesso.
  3. Execute com -race e valide a ausência de condições de corrida.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?