Fundamentos de Programação/02 - Código que Presta/Semana 07 - Testes Automatizados10 min
Mocks e Stubs
- 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) | — |
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
Atualizafoi 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:
- nada — a função é pura, só passe valores
- fake — implementação simplificada e reutilizável (repo em memória)
- stub — resposta fixa para um caso
- 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. httptestem 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 := ¬ifSpy{}
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
- 3. Dependency Inversion Principle — semana 6, interface pequena no consumidor é o que torna o dublê barato
- 1. Teste Unitário — verificar estado sobrevive a refatoração; verificar interação não
- 2. Teste de Integração — onde o wrapper do terceiro é testado contra o real
- 3. Interfaces — semana 5, satisfação implícita dispensa biblioteca de mock
Parte de Semana 07 - Testes Automatizados · 00 - MOC Fundamentos de Programação