No dia a dia, programadores usam "mock" como termo genérico para qualquer coisa que substitui uma dependência real durante teste. É confortável e gera comunicação suficiente em conversas casuais. O preço dessa imprecisão aparece quando os testes começam a doer: testes frágeis que quebram em qualquer refactor, testes lentos sem motivo aparente, testes que verificam coisas estranhas. A causa raiz costuma ser uso errado de um tipo específico de double — e quem não distingue os tipos não consegue nem formular o problema.
Gerald Meszaros, em xUnit Test Patterns (2007), formalizou o vocabulário que a comunidade adotou. Cinco categorias: dummy, stub, spy, mock, fake. Cada uma resolve um problema específico, e conhecê-las pelo nome não é teatro — é o que permite olhar para um teste e diagnosticar "este aqui está usando mock onde devia ser fake" antes de o teste virar problema. Martin Fowler, em Mocks Aren't Stubs, destrincha por que essas distinções importam para a qualidade dos testes.
O termo guarda-chuva — test double
"Test double" é o termo que abrange todos os tipos. A palavra vem do cinema: dublê é alguém que substitui o ator em cenas perigosas, parecendo o suficiente com o original para não estragar a tomada. Test doubles são a versão programática: substitutos de objetos reais durante testes, parecidos o suficiente para o código sob teste não notar — mas baratos para criar e controlar.
A questão não é "usar ou não doubles" — quase todo teste não-trivial usa algum. A questão é qual tipo, e por quê.
Os cinco tipos, com precisão
Dummy
O mais simples: um objeto passado mas nunca usado. Existe só para satisfazer assinatura de método ou construtor. Nunca tem método chamado nele durante o teste.
Caso típico: UserService(IUserRepository repo, ILogger logger)
e o teste vai exercitar um caminho que não loga nada. Você passa
null (se a linguagem aceita) ou um objeto vazio para
logger — é dummy. O importante é não ser usado.
// C# — dummy
var dummyLogger = NullLogger<UserService>.Instance;
var service = new UserService(realRepo, dummyLogger);
// teste exercita caminho que nunca loga
// Python — dummy via Mock que nunca é verificado
service = UserService(repo=real_repo, logger=Mock())
Dummies são raros em código bem-projetado. Frequentemente significam que o construtor está pedindo demais — você está sendo forçado a passar coisas que não vai usar.
Stub
Um stub devolve respostas pré-determinadas a chamadas. É passivo: não verifica nada, não registra chamadas — só fornece dados. Stubs são para situações onde o teste precisa controlar o que uma dependência retornaria.
Caso típico: você está testando OrderService.calculateTotal()
que precisa do preço atual do produto. O PriceService real
iria buscar de um banco; o stub devolve {1: 100, 2: 50}.
Teste corre rápido, determinístico.
// C# com NSubstitute — stub
var priceStub = Substitute.For<IPriceService>();
priceStub.GetPrice(1).Returns(100m);
priceStub.GetPrice(2).Returns(50m);
var service = new OrderService(priceStub);
service.CalculateTotal(new[] { 1, 2 }).Should().Be(150);
// não verifica que GetPrice foi chamado — só fornece valores
Stubs cobrem a maioria das necessidades de "controlar input vindo de colaborador". São baratos, claros, pouco frágeis.
Spy
Um spy é como um stub, mas registra as chamadas que recebeu. O teste pode inspecionar depois: foi chamado quantas vezes? Com que argumentos? Em que ordem? A diferença para mock é sutil mas importante: spy registra passivamente; o teste verifica o que quiser, depois.
// Spy "manual" em Go
type spyNotifier struct {
calls []notifyCall
}
type notifyCall struct{ to, msg string }
func (s *spyNotifier) Notify(to, msg string) error {
s.calls = append(s.calls, notifyCall{to, msg})
return nil
}
// No teste:
spy := &spyNotifier{}
service := NewOrderService(repo, spy)
service.Confirm("abc")
// Verificações flexíveis sobre o registro:
if len(spy.calls) != 1 { t.Errorf(...) }
if spy.calls[0].to != "user@x.com" { t.Errorf(...) }
Spies em Go são naturais — sem framework, você escreve uma struct que
gravita um array. Em C# e Python, frameworks (NSubstitute, Mock) podem
atuar como spy se você só consultar Received() ou
call_args_list sem configurar expectativas estritas.
Mock
Mock é stub + verificação ativa e estrita. Você
configura previamente as expectativas: "espero que Notify
seja chamado uma vez, com argumento X". Se o objeto sob teste não cumprir
exatamente, o teste falha. Se chamar algo não-esperado, falha. Se chamar
em ordem errada, falha.
Mocks vêm de Mock Objects, paper de Tim Mackinnon, Steve Freeman e Philip Craig (2000). A intenção original era específica: usar mocks como ferramenta de design — você descobre quais colaborações precisa antes de implementá-las.
// C# com NSubstitute — mock estrito
var notifier = Substitute.For<INotifier>();
var service = new OrderService(repo, notifier);
service.Confirm("abc");
// Verificação ESTRITA (mock-style):
notifier.Received(1).Notify("user@x.com", Arg.Any<string>());
notifier.DidNotReceive().Notify(Arg.Any<string>(), "spam");
O problema com mocks é que eles produzem testes que verificam como o código chama colaboradores, não o que ele produz. Se você refatora a implementação mantendo comportamento, mocks frequentemente quebram — as chamadas internas mudaram. É a fragilidade mais comum.
Quando você se vê configurando 5+ mocks num único teste, com verificações estritas em todos, pare. Esse teste virou fotografia da implementação, e qualquer refactor vai destruí-lo. Frequentemente significa que a fronteira testada está mal-posta — você está testando algo que faz coisas demais. Considere quebrar em testes menores ou revisar o design.
Fake
Um fake é uma implementação alternativa real, mas simplificada, do mesmo contrato. Não é stub (que devolve dados pré-fixados); é uma implementação funcional que faz a coisa, só de forma simplificada.
Exemplo canônico: InMemoryUserRepository que implementa a
mesma interface do PostgresUserRepository, guardando dados
num Dictionary. Você pode salvar, buscar, atualizar,
apagar — tudo funciona, só não persiste. Para testes, comportamento é
indistinguível.
// Fake real em C#
public class InMemoryUserRepository : IUserRepository {
private readonly Dictionary<Guid, User> _users = new();
public Task Save(User u) {
_users[u.Id] = u;
return Task.CompletedTask;
}
public Task<User?> FindById(Guid id) =>
Task.FromResult(_users.GetValueOrDefault(id));
public Task<bool> Delete(Guid id) =>
Task.FromResult(_users.Remove(id));
}
// Uso no teste — sem mock framework
var repo = new InMemoryUserRepository();
await repo.Save(new User { Id = userId, Name = "Alice" });
var service = new UserService(repo);
await service.UpdateName(userId, "Alice Smith");
var updated = await repo.FindById(userId);
updated.Name.Should().Be("Alice Smith");
Fakes têm várias vantagens sobre mocks:
- Comportamento real: o fake faz o que faz; o teste verifica o resultado, não as chamadas. Refactor não quebra.
- Reutilizável: o mesmo fake serve dezenas de testes, sem setup repetitivo.
- Documentação viva: ler o fake é entender o contrato que ele implementa.
- Catches errors no contrato: se você muda a interface, o compilador (em linguagens tipadas) força você a atualizar o fake — evita drift.
A objeção típica a fakes é "trabalho de manter um fake completo". Em pequenos casos, vale. Em grandes, vale mais ainda — mas exige compromisso de mantê-lo paralelo ao real. Em prática, fakes acabam sendo escritos por necessidade e ficando como utilidade.
A heurística de Fowler — "Mocks Aren't Stubs"
No texto canônico, Fowler organiza a decisão de uso em torno de uma pergunta central: o teste verifica estado ou interação?
- Verificação de estado: depois de executar a operação, olhamos no objeto/banco/sistema e checamos se o estado ficou correto. Stubs e fakes cabem.
- Verificação de interação: depois de executar a operação, perguntamos "o objeto X foi chamado da forma Y?". Spies e mocks cabem.
A maioria dos casos é melhor verificar estado — porque resultado é o que importa, e qualquer caminho que produza resultado correto é aceitável. Verificação de interação faz sentido onde o resultado é a interação em si: enviar um e-mail, registrar log, publicar mensagem em fila.
O mesmo teste com diferentes doubles
O mesmo cenário — testar que OrderService.confirm() envia
notificação ao cliente — pode ser testado de várias formas. Cada uma
tem trade-offs.
Estilo mockist (mock estrito)
[Fact]
public async Task Confirm_SendsNotification() {
var repo = new InMemoryOrderRepository();
repo.Save(new Order { Id = "abc", Email = "u@x.com" });
var notifier = Substitute.For<INotifier>();
var service = new OrderService(repo, notifier);
await service.Confirm("abc");
// Mock-style: verifica chamada
await notifier.Received(1).Notify("u@x.com", Arg.Any<string>());
}
Acoplado a "como": se a implementação mudar para usar fila em vez de chamada direta, teste quebra mesmo se comportamento permanecer (mensagem chegou).
Estilo classicist (estado via fake)
[Fact]
public async Task Confirm_SendsNotification() {
var repo = new InMemoryOrderRepository();
repo.Save(new Order { Id = "abc", Email = "u@x.com" });
var fakeNotifier = new InMemoryNotifier(); // fake real
var service = new OrderService(repo, fakeNotifier);
await service.Confirm("abc");
// Verifica resultado: notificação foi efetivamente enviada
fakeNotifier.SentMessages.Should().ContainSingle()
.Which.To.Should().Be("u@x.com");
}
Verifica que o resultado aconteceu — que a notificação está registrada no fake (e por extensão, teria sido enviada). Refactor da implementação (chamada direta vs fila) não quebra o teste enquanto o resultado for o mesmo.
Eventos como saída
Uma terceira abordagem é fazer o domínio emitir eventos, e testar os eventos:
[Fact]
public void Confirm_EmitsOrderConfirmedEvent() {
var order = new Order { Id = "abc", Email = "u@x.com" };
order.Confirm();
order.Events.Should().ContainSingle()
.Which.Should().BeOfType<OrderConfirmed>()
.Which.Email.Should().Be("u@x.com");
}
Aqui o domínio é puro — só registra eventos. Aplicação reage aos eventos (envia notificação, atualiza estoque, publica em fila). O teste do domínio é simples; o teste da aplicação verifica que eventos viram ações. Padrão muito usado em DDD e Event Sourcing — separa "decisão" de "efeito".
O mesmo problema nas três linguagens
Cada linguagem tem ferramental e idioms diferentes. Mesmo padrão, sintaxes distintas:
// Stub — só fornece dados
var priceStub = Substitute.For<IPriceService>();
priceStub.GetPrice(1).Returns(100m);
// Spy/Mock — verificação de chamada
var notifier = Substitute.For<INotifier>();
service.Confirm(orderId);
await notifier.Received(1).Notify(Arg.Any<string>(), Arg.Any<string>());
// Fake — implementação alternativa
public class InMemoryOrderRepo : IOrderRepository {
private readonly Dictionary<string, Order> _store = new();
public Task Save(Order o) { _store[o.Id] = o; return Task.CompletedTask; }
public Task<Order?> FindById(string id) =>
Task.FromResult(_store.GetValueOrDefault(id));
}
NSubstitute, Moq, FakeItEasy são as três principais bibliotecas em .NET. NSubstitute tem sintaxe mais moderna. Para fakes, implementação manual é o caminho.
# Stub — Mock com return_value
price_stub = Mock(spec=PriceService)
price_stub.get_price.return_value = 100
# Spy/Mock — verificação
notifier = Mock(spec=Notifier)
service.confirm(order_id)
notifier.notify.assert_called_once_with("u@x.com", ANY)
notifier.notify.call_args_list # se quiser inspecionar
# Fake — classe normal implementando o protocolo
class InMemoryOrderRepo:
def __init__(self):
self._store = {}
def save(self, order: Order) -> None:
self._store[order.id] = order
def find_by_id(self, id: str) -> Order | None:
return self._store.get(id)
spec=Notifier faz Mock só aceitar chamadas válidas
do contrato — evita "drift" quando interface muda. Em Python,
Protocols permitem fakes sem precisar herdar nada.
// Stub manual
type stubPrice struct{}
func (s stubPrice) GetPrice(id int) (int, error) {
if id == 1 { return 100, nil }
return 0, ErrNotFound
}
// Spy manual
type spyNotifier struct {
calls []notifyCall
}
type notifyCall struct{ to, msg string }
func (s *spyNotifier) Notify(to, msg string) error {
s.calls = append(s.calls, notifyCall{to, msg})
return nil
}
// Fake (in-memory)
type inMemoryOrderRepo struct {
store map[string]Order
}
func (r *inMemoryOrderRepo) Save(o Order) error {
r.store[o.ID] = o
return nil
}
func (r *inMemoryOrderRepo) FindByID(id string) (Order, error) {
o, ok := r.store[id]
if !ok { return Order{}, ErrNotFound }
return o, nil
}
Go favorece fakes manuais sobre frameworks de mock. Mais verboso, mas explícito — código de teste tem mesma forma do código de produção. Mais difícil de errar, mais fácil de ler.
Quando usar cada um
Heurísticas práticas:
- Use fake quando você tem um contrato bem-definido (interface, protocol, abstract class) e a dependência tem comportamento que vale simular. Repositórios, caches, message brokers, clocks. Fake reusável amortiza o custo.
- Use stub quando você só precisa controlar o que uma dependência retornaria, sem se importar como ou quando ela é chamada. Especialmente útil para testes de cálculo onde inputs vêm de fora.
- Use spy quando o teste precisa verificar chamadas mas você não quer compromisso estrito do mock. Mais flexível, menos frágil.
- Use mock quando o efeito da operação é a chamada, e o teste precisa garantir que ela aconteceu. Notificações, publicação em fila, invalidação de cache. E mesmo aqui, considere spy ou fake antes — frequentemente cabem.
- Use dummy só quando você é forçado pelo construtor. Se há muitos dummies, considere se o construtor está pedindo demais.
O que evitar
Mockar tudo por reflexo
"Vou mockar todas as dependências" é antipattern. Cada mock é acoplamento adicional do teste à implementação. Quando 80% do teste é setup de mocks, ele virou documentação de chamadas internas, não verificação de comportamento. Fakes resolvem a maioria desses casos sem prejuízo.
Mock de classes próprias do projeto
Você só deveria mockar nas fronteiras — coisas que vêm de fora do seu código (banco, HTTP, file system). Mockar suas próprias classes significa que elas estão acopladas demais — você precisa quebrar a cadeia. Foco da abstração foi perdido.
Verificação de interação onde estado bastaria
Verificar que repo.Save foi chamado é mais frágil do que
verificar que o objeto está salvo. Use o fake e verifique
fakeRepo.Get(id). Refactor da implementação não quebra.
Reset incompleto entre testes
Fakes que mantêm estado entre testes são bomba-relógio. Cada teste
deveria começar com fake limpo. Use fixtures, factories, ou
setUp/setup_method da framework para
garantir isolamento. Testes que dependem de ordem são pesadelo de
manutenção.
Como praticar
- Auditoria de testes existentes. Pegue uma suíte sua e classifique cada teste pelo tipo de double que usa. Identifique candidatos a converter de mock para fake. Comece pelos que mais quebram em refactors.
- Escreva o mesmo teste das três formas. Pegue um cenário simples (notificação após pedido confirmado). Implemente com mock estrito, com fake + verificação de estado, e com eventos. Compare legibilidade, fragilidade, manutenibilidade.
-
Crie um fake substancial. Para o repositório do
projeto deste módulo (ou seu projeto atual), implemente
InMemoryRepositorycompleto, com mesmos métodos. Use-o em vários testes. Note como o teste fica sem ceremony.
Referências para aprofundar
- livro xUnit Test Patterns: Refactoring Test Code — Gerard Meszaros (2007).
- livro Growing Object-Oriented Software, Guided by Tests — Freeman & Pryce (2009).
- livro Unit Testing Principles, Practices, and Patterns — Vladimir Khorikov (2020).
- artigo Mocks Aren't Stubs — Martin Fowler.
- artigo Test Double — Martin Fowler bliki.
- artigo Test contra-passing fakes — Hillel Wayne.
- artigo Don't Mock Types You Don't Own — Steve Freeman.
- docs NSubstitute documentation.
- docs unittest.mock — Python stdlib.
- docs testify — Go testing toolkit.
- vídeo Magic Tricks of Testing — Sandi Metz (RailsConf 2013).
- vídeo TDD, Where Did It All Go Wrong — Ian Cooper.