trilha

Fundamentos de Programação/02 - Código que Presta/Semana 07 - Testes Automatizados10 min

Mocks e Stubs

Perguntas-guia
  • Qual a diferença real entre dublê, stub, mock, spy e fake?
  • Por que testar tudo com mock é uma armadilha? (o teste passa e a produção quebra)
  • O que significa "não mocke o que você não possui"?
  • Quando um mock indica que o design está acoplado demais?

Conceito

"Mock" virou nome genérico para qualquer dublê, e a confusão custa caro — porque os tipos têm efeitos opostos sobre a qualidade do teste. A taxonomia (Meszaros, popularizada por Fowler):

Dublê O que faz Verifica
dummy é passado e nunca usado nada
stub devolve respostas pré-programadas
spy stub que registra as chamadas você inspeciona depois
mock tem expectativas e falha se não forem cumpridas interação
fake implementação funcional simplificada (repo em memória)
A distinção que decide a qualidade do teste

Stub e fake permitem verificar estado. Mock verifica interação.

  • Verificar estado: "depois de cancelar, o pedido está cancelado". Isso é comportamento — sobrevive a refatoração.
  • Verificar interação: "o método Atualiza foi chamado exatamente uma vez com este argumento". Isso é implementação — quebra quando você refatora sem mudar comportamento.

Mock acopla o teste ao como. É por isso que "mockar tudo" produz suítes que quebram a cada mudança estrutural e não pegam bug nenhum.

Por que testar tudo com mock é uma armadilha, mecanicamente: você escreve no teste a sua crença sobre como o colaborador se comporta. O teste então verifica que o seu código conversa com a sua crença. Se a crença está errada, o teste passa e a produção quebra — o teste não tem como saber.

"Não mocke o que você não possui." Ao mockar a API de um terceiro, você codificou uma suposição sobre o comportamento dela. Quando ela devolver um campo a menos, um 429, ou um formato de data diferente, seu mock continua devolvendo o que você imaginou. A saída correta: envolva o terceiro numa interface sua, mocke o seu wrapper nos testes de unidade, e teste o wrapper contra o real na integração (2. Teste de Integração).

Quando um mock indica design acoplado: se para testar uma função você precisa preparar cinco dublês, essa função conhece cinco colaboradores. O problema não é o teste — é a função. Dificuldade de testar é um sinal de projeto, e é o argumento central do 5. TDD.

A ordem de preferência, na prática:

  1. nada — a função é pura, só passe valores
  2. fake — implementação simplificada e reutilizável (repo em memória)
  3. stub — resposta fixa para um caso
  4. mock — só quando a interação em si é o comportamento (ex.: "publica exatamente um evento")

Em Go

A satisfação implícita de interfaces torna dublê barato ao ponto de não precisar de biblioteca. Se a interface está declarada no consumidor com um ou dois métodos (3. Dependency Inversion Principle), o fake tem três linhas:

type repoFalso struct{ p Pedido; err error }
func (r repoFalso) Busca(int) (Pedido, error) { return r.p, r.err }

Nenhuma geração de código, nenhum when(...).thenReturn(...), nenhum framework.

O stub mais leve de Go: campos de função. Você controla o comportamento por caso, sem criar um tipo por cenário:

type repoStub struct {
	BuscaFn func(int) (Pedido, error)
}
func (r repoStub) Busca(id int) (Pedido, error) { return r.BuscaFn(id) }

// no teste:
repo := repoStub{BuscaFn: func(int) (Pedido, error) { return Pedido{}, ErrNaoEncontrado }}

Este é o idioma mais útil da nota. Um tipo, todos os cenários.

Bibliotecas existem e a comunidade se divide: gomock (Google) e mockery geram mocks a partir da interface. Elas ganham quando a interface é grande — e o contra-argumento é que interface grande é o problema, não a falta de gerador.

Para dependências HTTP, httptest.NewServer é melhor que mock e é a recomendação da própria stdlib: ele exercita serialização real, status code real, timeout real. Um mock do *http.Client verifica que você chamou um método; o httptest verifica que você fala HTTP corretamente.

Tempo é a dependência mais esquecida. time.Now() chamado direto torna o teste não-determinístico. O idioma é injetar:

type Servico struct{ agora func() time.Time }
// produção: Servico{agora: time.Now}
// teste:    Servico{agora: func() time.Time { return dataFixa }}

Aleatoriedade, idem: injete *rand.Rand com semente fixa em vez de usar o global.

Respostas às perguntas-guia

1. Qual a diferença real entre dublê, stub, mock, spy e fake?

Conceito: dummy não é usado; stub responde; spy registra; mock exige e verifica interação; fake é uma implementação simplificada de verdade.

Em Go: fake e stub são structs de poucas linhas; spy é um fake com um slice de chamadas; mock costuma vir de gerador. A ordem de preferência idiomática é fake > stub > mock.

2. Por que testar tudo com mock é uma armadilha?

Conceito: porque você verifica que o código conversa com a sua crença sobre o colaborador. Crença errada → teste verde, produção vermelha. E verificar interação amarra o teste à implementação.

Em Go: o sintoma é o teste que quebra quando você troca duas chamadas de ordem, ou extrai um método — mudanças que não alteram comportamento nenhum.

3. O que significa "não mocke o que você não possui"?

Conceito: mockar terceiro codifica sua suposição sobre ele. Envolva numa interface sua, mocke o wrapper, e teste o wrapper contra o real.

Em Go: o wrapper é uma interface pequena declarada no seu pacote, e o teste do wrapper usa httptest.NewServer devolvendo respostas reais — inclusive as feias (429, 500, corpo truncado, JSON inválido).

4. Quando um mock indica que o design está acoplado demais?

Conceito: quando testar uma função exige preparar muitos dublês. A função conhece coisas demais.

Em Go, o limiar prático: três dublês para um teste é sinal amarelo. Quase sempre a correção é extrair a regra pura (que não precisa de dublê nenhum) do código de I/O (2. Single Responsibility Principle).

Trade-offs

Do conceito:

  • Fake compra realismo e reúso; cobra ser mantido (ele é código de produção disfarçado).
  • Stub compra simplicidade e cobra que você duplique o cenário em cada teste.
  • Mock compra verificação de interação — o que é o comportamento correto a verificar em alguns casos (publicar evento, enviar e-mail) e o errado na maioria.
  • Não usar dublê nenhum é sempre o melhor, quando o código permite.

Em Go:

  • Interface pequena + satisfação implícita torna dublê à mão mais barato que gerar. O custo é escrever alguns métodos vazios.
  • Gerador (gomock) compra conveniência com interface grande e cobra um passo de build e testes que verificam interação por padrão.
  • httptest em vez de mock de cliente compra fidelidade quase de graça.
  • Injetar relógio e aleatoriedade custa um campo e compra determinismo — e é a correção de teste instável mais comum.

Exemplo prático

package main

import (
	"errors"
	"fmt"
	"testing"
	"time"
)

// ==================== domínio ====================
type Assinatura struct {
	ID      int
	Vence   time.Time
	Ativa   bool
}

var ErrNaoEncontrada = errors.New("assinatura não encontrada")

// interfaces pequenas, declaradas pelo consumidor
type repositorio interface {
	Busca(id int) (Assinatura, error)
	Salva(a Assinatura) error
}
type notificador interface {
	Envia(para, msg string) error
}

type Renovador struct {
	repo  repositorio
	notif notificador
	agora func() time.Time // DEPENDÊNCIA INJETADA: sem isto, teste não é determinístico
}

func (r Renovador) Renova(id int, dias int) error {
	a, err := r.repo.Busca(id)
	if err != nil {
		return fmt.Errorf("renovar %d: %w", id, err)
	}
	base := r.agora()
	if a.Vence.After(base) {
		base = a.Vence // ainda válida: soma ao vencimento, não a hoje
	}
	a.Vence = base.AddDate(0, 0, dias)
	a.Ativa = true
	if err := r.repo.Salva(a); err != nil {
		return fmt.Errorf("renovar %d: %w", id, err)
	}
	_ = r.notif.Envia("cliente", "assinatura renovada")
	return nil
}

// ==================== 1. FAKE: implementação simplificada e reutilizável ====================
type repoFake struct{ dados map[int]Assinatura }

func novoRepoFake(as ...Assinatura) *repoFake {
	r := &repoFake{dados: map[int]Assinatura{}}
	for _, a := range as {
		r.dados[a.ID] = a
	}
	return r
}
func (r *repoFake) Busca(id int) (Assinatura, error) {
	a, ok := r.dados[id]
	if !ok {
		return Assinatura{}, ErrNaoEncontrada
	}
	return a, nil
}
func (r *repoFake) Salva(a Assinatura) error { r.dados[a.ID] = a; return nil }

// ==================== 2. STUB por campo de função: um tipo, todos os cenários ====================
type repoStub struct {
	BuscaFn func(int) (Assinatura, error)
	SalvaFn func(Assinatura) error
}

func (r repoStub) Busca(id int) (Assinatura, error) { return r.BuscaFn(id) }
func (r repoStub) Salva(a Assinatura) error {
	if r.SalvaFn == nil {
		return nil
	}
	return r.SalvaFn(a)
}

// ==================== 3. SPY: fake que registra ====================
type notifSpy struct{ enviadas []string }

func (n *notifSpy) Envia(_, msg string) error {
	n.enviadas = append(n.enviadas, msg)
	return nil
}

type notifNulo struct{}

func (notifNulo) Envia(string, string) error { return nil }

// ==================== testes ====================
var relogioFixo = func() time.Time {
	return time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC)
}

// VERIFICA ESTADO com fake: sobrevive a refatoração
func TestRenova_VerificandoEstado(t *testing.T) {
	casos := []struct {
		nome      string
		vence     time.Time
		dias      int
		wantVence time.Time
	}{
		{
			"expirada soma a partir de hoje",
			time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
			30,
			time.Date(2026, 3, 31, 12, 0, 0, 0, time.UTC),
		},
		{
			"válida soma a partir do vencimento",
			time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC),
			30,
			time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC),
		},
	}
	for _, c := range casos {
		t.Run(c.nome, func(t *testing.T) {
			repo := novoRepoFake(Assinatura{ID: 1, Vence: c.vence})
			r := Renovador{repo: repo, notif: notifNulo{}, agora: relogioFixo}

			if err := r.Renova(1, c.dias); err != nil {
				t.Fatalf("err = %v", err)
			}
			// verifica o ESTADO final, não quais métodos foram chamados
			got := repo.dados[1]
			if !got.Vence.Equal(c.wantVence) {
				t.Errorf("Vence = %v; want %v", got.Vence, c.wantVence)
			}
			if !got.Ativa {
				t.Error("deveria estar ativa")
			}
		})
	}
}

// STUB por campo de função: cenários de erro sem criar um tipo para cada
func TestRenova_ErrosDeInfra(t *testing.T) {
	casos := []struct {
		nome string
		repo repositorio
		want string
	}{
		{
			"não encontrada",
			repoStub{BuscaFn: func(int) (Assinatura, error) {
				return Assinatura{}, ErrNaoEncontrada
			}},
			"renovar 1: assinatura não encontrada",
		},
		{
			"falha ao salvar",
			repoStub{
				BuscaFn: func(int) (Assinatura, error) { return Assinatura{ID: 1}, nil },
				SalvaFn: func(Assinatura) error { return errors.New("disco cheio") },
			},
			"renovar 1: disco cheio",
		},
	}
	for _, c := range casos {
		t.Run(c.nome, func(t *testing.T) {
			r := Renovador{repo: c.repo, notif: notifNulo{}, agora: relogioFixo}
			err := r.Renova(1, 30)
			if err == nil || err.Error() != c.want {
				t.Errorf("err = %v; want %q", err, c.want)
			}
		})
	}
}

// SPY: aqui a INTERAÇÃO é o comportamento — notificar É o requisito
func TestRenova_NotificaUmaVez(t *testing.T) {
	repo := novoRepoFake(Assinatura{ID: 1})
	spy := &notifSpy{}
	r := Renovador{repo: repo, notif: spy, agora: relogioFixo}

	if err := r.Renova(1, 30); err != nil {
		t.Fatalf("err = %v", err)
	}
	if len(spy.enviadas) != 1 {
		t.Errorf("notificações = %d; want 1", len(spy.enviadas))
	}
}

// O relógio injetado é o que torna tudo acima determinístico.
// Sem ele, "soma 30 dias a partir de hoje" mudaria de resposta todo dia.
func TestRelogioInjetadoTornaDeterministico(t *testing.T) {
	repo := novoRepoFake(Assinatura{ID: 1})
	r := Renovador{repo: repo, notif: notifNulo{}, agora: relogioFixo}
	_ = r.Renova(1, 1)
	primeiro := repo.dados[1].Vence

	repo2 := novoRepoFake(Assinatura{ID: 1})
	r2 := Renovador{repo: repo2, notif: notifNulo{}, agora: relogioFixo}
	_ = r2.Renova(1, 1)

	if !primeiro.Equal(repo2.dados[1].Vence) {
		t.Error("resultado deveria ser idêntico com relógio fixo")
	}
	if primeiro.Format("2006-01-02") != "2026-03-02" {
		t.Errorf("vence em %v; esperado 2026-03-02", primeiro)
	}
}

Compare os dois estilos de verificação nos testes acima. TestRenova_VerificandoEstado pergunta "a assinatura acabou com o vencimento certo?" — e continua passando se você reorganizar o corpo de Renova inteiro. TestRenova_NotificaUmaVez verifica interação, e é o caso legítimo para isso: "notificar o cliente" é o requisito, não um detalhe de como o requisito foi cumprido.

Relacionado


Parte de Semana 07 - Testes Automatizados · 00 - MOC Fundamentos de Programação

Buscar

Busca por título, seção e texto das notas