MÓDULO 02 · CONCEITO 03 DE 8

Test doubles — vocabulário preciso

Dummy, stub, spy, mock, fake. Conhecer os nomes não é academicismo — é o que permite escolher a ferramenta certa.

Tempo de leitura ~22 min Pré-requisito Pirâmide, Troféu & Honeycomb Próximo BDD & Given/When/Then

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.

armadilha clássica

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:

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?

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:

C# — NSubstitute
// 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.

Python — unittest.mock + fakes
# 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.

Go — fakes manuais (idiomático)
// 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:

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

  1. 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.
  2. 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.
  3. Crie um fake substancial. Para o repositório do projeto deste módulo (ou seu projeto atual), implemente InMemoryRepository completo, com mesmos métodos. Use-o em vários testes. Note como o teste fica sem ceremony.

Referências para aprofundar

  1. livro xUnit Test Patterns: Refactoring Test Code — Gerard Meszaros (2007). Catálogo gigante de patterns e antipatterns. Capítulo "Test Double Patterns" é a fonte da taxonomia. Use como referência.
  2. livro Growing Object-Oriented Software, Guided by Tests — Freeman & Pryce (2009). Aprofundamento da escola "London" — usa mocks como ferramenta de design. Mesmo se você não comprar a filosofia, vale ler para entender a posição.
  3. livro Unit Testing Principles, Practices, and Patterns — Vladimir Khorikov (2020). Tratamento moderno e equilibrado. Khorikov defende classicist com nuance. Capítulos 5 e 8 sobre mocks e fakes são excelentes.
  4. artigo Mocks Aren't Stubs — Martin Fowler. martinfowler.com/articles/mocksArentStubs.html — texto canônico. Releia sempre que estiver em dúvida sobre qual usar.
  5. artigo Test Double — Martin Fowler bliki. martinfowler.com/bliki/TestDouble.html — definição curta de cada tipo. Use como cheat sheet.
  6. artigo Test contra-passing fakes — Hillel Wayne. buttondown.email/hillelwayne — discussão sobre quando fakes "passam quando não deviam" e como mitigar.
  7. artigo Don't Mock Types You Don't Own — Steve Freeman. Princípio canônico: mocke suas próprias abstrações, não tipos de bibliotecas externas. Wraps explicitamente.
  8. docs NSubstitute documentation. nsubstitute.github.io — biblioteca com sintaxe moderna. Compare com Moq para escolher.
  9. docs unittest.mock — Python stdlib. docs.python.org/3/library/unittest.mock.html — fonte oficial. Veja seção "spec" para evitar drift.
  10. docs testify — Go testing toolkit. github.com/stretchr/testify — biblioteca popular. Veja "mock" subpackage para casos onde fake manual fica trabalhoso.
  11. vídeo Magic Tricks of Testing — Sandi Metz (RailsConf 2013). YouTube. Heurísticas concretas sobre o que testar e o que não. Mocks vs fakes aparecem implicitamente. Em Ruby, universal conceitualmente.
  12. vídeo TDD, Where Did It All Go Wrong — Ian Cooper. YouTube. Crítica forte ao "mockismo" excessivo. Argumenta por testar comportamento via fakes. Provocativo, formativo.