MÓDULO 02 · CONCEITO 08 DE 8

Snapshot, fixtures & testes flaky

Os tópicos práticos que decidem se sua suíte é aliada ou inimiga — e como exterminar o flaky test.

Tempo de leitura ~22 min Pré-requisito Contract testing Próximo Módulo 03 — Bancos de Dados

Os sete conceitos anteriores cobriram a teoria e os métodos. Este último é onde mora o cotidiano: como produzir dados de teste sem duplicação infinita, como testar saídas grandes sem assertions gigantescos, e como lidar com o pior pesadelo da engenharia de qualidade — o teste que falha 5% das vezes sem motivo aparente. Cada um desses tópicos é onde times realmente se diferenciam: a teoria de TDD é mais ou menos a mesma para todos, mas decidir como construir uma Order de teste, como verificar que um e-mail gerado bate com o esperado, e o que fazer quando o teste de ontem falhou hoje sem ninguém ter mexido em nada — isso separa equipes que mantém suítes saudáveis de equipes que aos poucos param de confiar em CI.

Vou cobrir três tópicos que parecem distintos mas são profundamente conectados: snapshot tests, fixtures e builders de dados, e flaky tests. O fio que conecta os três é a busca por testes que sejam ao mesmo tempo úteis (pegam bugs reais), baratos (não custam mais do que valem) e confiáveis (passam ou falham por motivos reais). Quando um desses três se quebra, a suíte começa a degradar.

Snapshot tests — quando o output é grande demais para um assert

Suponha que sua função gera um e-mail de confirmação de pedido — template HTML de algumas páginas, com variáveis preenchidas. Como você testa isso? assert email == "<html>...30 linhas..." é insuportável. Verificar campo por campo (email contém o nome, email contém o total) é frágil — pega só o que você lembrou de verificar. Erros de formatação passam despercebidos.

A solução: snapshot tests. Você executa a função uma vez, salva o output como "snapshot" em um arquivo. Próximas execuções comparam o output atual com o snapshot salvo. Se forem iguais, passa; se diferentes, falha mostrando o diff.

Adoção em comunidades:

Quando snapshot brilha

Quando snapshot dói

O método tem patologias específicas que aparecem quando aplicado sem critério.

Aprovação automática viciante

Mudou alguma coisa, snapshot quebrou. "Ah, é só atualizar." Time atualiza sem ler o diff. Em algum momento, um bug real é "atualizado" sem ninguém perceber — o output errado virou o novo "esperado".

Disciplina: cada atualização de snapshot exige review consciente do diff. Não automatize "atualize tudo". O snapshot é o assert; aprovar mudança no snapshot é aprovar mudança no comportamento. Fluxo no PR: revisor vê diff de snapshots, valida se mudança é intencional.

Outputs com elementos voláteis

Timestamp atual, IDs gerados, hash de coisa aleatória. Cada execução produz output diferente; snapshot quebra todo dia. A solução é scrubbing: substituir partes voláteis por placeholders antes de comparar.

// C# com Verify — scrubber para timestamps
[Fact]
public Task GeneratesEmail() {
    var email = generator.Generate(order);

    return Verify(email)
        .ScrubInlineGuids()
        .ScrubMember("CreatedAt")
        .ScrubLinesContaining("X-Trace-Id");
}

Ou no código, gerar com clock injetado e fixá-lo no teste:

// Clock injetado — preferível
public EmailGenerator(IClock clock) { _clock = clock; }

[Fact]
public Task GeneratesEmail() {
    var fixedClock = new FakeClock(DateTimeOffset.Parse("2025-01-15T10:00:00Z"));
    var generator = new EmailGenerator(fixedClock);
    return Verify(generator.Generate(order));
}

Snapshot grande demais para revisar

Snapshot de 500 linhas. Diff de 50 linhas no PR. Revisor não vai ler. Sintoma: o snapshot tem coisa demais; está testando interfaces que mudam por motivos não relacionados à mudança em questão. Quebre em snapshots menores e focados.

Testes acoplados a representação

Você muda formatação de JSON (espaços, ordem de campos) por motivo legítimo — nada quebrou em produção. Mas trezentos snapshots quebraram. Se a representação não é estável, snapshot vira manutenção pesada. Use formatação canônica (sorted keys, whitespace normalizado) ou compare semanticamente, não como string.

heurística do sênior

Use snapshot para outputs grandes e relativamente estáveis. Não use para resultados que poderiam ser verificados claramente com 2-3 asserts específicos. order.total == 150 é mais robusto e mais legível que snapshot do order inteiro.

Fixtures e builders — produzindo dados de teste

Cada teste de domínio precisa criar objetos de domínio: Order, User, Product. A forma como você produz esses dados decide muito sobre legibilidade e manutenibilidade da suíte. Há um espectro de abordagens, do pior para o melhor.

Antipattern: criação inline duplicada

// Em CADA teste
var order = new Order(
    id: Guid.NewGuid(),
    customerId: Guid.NewGuid(),
    items: new[] {
        new OrderItem(productId: Guid.NewGuid(), price: 100, quantity: 1)
    },
    shippingAddress: new Address("Rua A", 123, "SP", "12345-678"),
    paymentMethod: PaymentMethod.CreditCard,
    createdAt: DateTime.UtcNow
);
// 10 linhas só para ter um order válido

Em 50 testes, são 500 linhas só de setup. Cada vez que você adiciona campo obrigatório a Order, precisa atualizar todos os 50. Mudança no domínio fica cara desproporcionalmente.

Tentativa: shared fixture

public static class TestData {
    public static Order ValidOrder() => new Order(...);
    public static Order LargeOrder() => new Order(...);
    public static Order CancelledOrder() => new Order(...);
}

[Fact]
void Test_SomeOrderBehavior() {
    var order = TestData.ValidOrder();
    // ...
}

Melhor que duplicação — mas surge novo problema: você precisa variar pequenos campos. "ValidOrder mas com 5 itens", "ValidOrder mas cancelada", "ValidOrder mas no Rio". Soluções: ValidOrder_5Items(), ValidOrder_Cancelled_RJ()... Helpers se multiplicam exponencialmente.

Padrão: Test Data Builder

Builder fluente que cria objeto válido por default, e permite sobrescrever campos específicos:

public class OrderBuilder {
    private Guid _id = Guid.NewGuid();
    private Guid _customerId = Guid.NewGuid();
    private List<OrderItem> _items = new();
    private Address _shipping = new("Rua A", 123, "SP", "12345-678");
    private PaymentMethod _payment = PaymentMethod.CreditCard;
    private DateTime _createdAt = new DateTime(2025, 1, 1);

    public OrderBuilder WithId(Guid id) {
        _id = id;
        return this;
    }
    public OrderBuilder WithItem(decimal price, int quantity = 1) {
        _items.Add(new OrderItem(Guid.NewGuid(), price, quantity));
        return this;
    }
    public OrderBuilder ShippingTo(string state) {
        _shipping = _shipping with { State = state };
        return this;
    }
    public OrderBuilder PaidWith(PaymentMethod method) {
        _payment = method;
        return this;
    }

    public Order Build() {
        if (_items.Count == 0) WithItem(100); // default item
        return new Order(_id, _customerId, _items, _shipping,
                         _payment, _createdAt);
    }
}

// Uso
var smallOrder = new OrderBuilder().Build();
var bigOrder = new OrderBuilder()
    .WithItem(price: 100, quantity: 10)
    .WithItem(price: 500)
    .Build();
var rioOrder = new OrderBuilder().ShippingTo("RJ").Build();

O builder:

Test Data Builders são padrão canônico, descrito por Nat Pryce e popularizado em Growing Object-Oriented Software. Em projetos sérios, é raro não usá-los.

Faker / autofixture — para dados sintéticos

Para casos onde você não se importa com valores específicos — "qualquer email válido", "qualquer endereço" — bibliotecas geram dados sintéticos:

Cuidado para não usar Faker em testes que verificam valores — "test_user_email_is_valid" não pode usar email aleatório do Faker sem validar o esperado. Faker é para noise, não para dados significativos do teste.

Object Mother — alternativa ao Builder

Padrão alternativo: classes de "Mother" que produzem variações nomeadas:

public static class Orders {
    public static Order Valid() => new OrderBuilder().Build();
    public static Order Pending() => new OrderBuilder().WithStatus(Status.Pending).Build();
    public static Order Cancelled() => new OrderBuilder().WithStatus(Status.Cancelled).Build();
    public static Order Large() => new OrderBuilder().WithItems(20).Build();
}

[Fact]
void Test() {
    var order = Orders.Cancelled();
    // ...
}

Trade-off: Object Mother é mais legível para casos canônicos, mas Builder é mais flexível para variações ad-hoc. Times maduros frequentemente usam ambos: Mother para casos nomeados, Builder para customizações.

Flaky tests — o cancro silencioso

Um teste flaky é aquele que passa às vezes e falha às vezes sem mudança no código. Você roda agora, falha; roda de novo, passa. Sem mudança, sem motivo aparente. Esse é o problema mais corrosivo de uma suíte de testes — pior que cobertura baixa, pior que testes lentos. Por quê?

O Google publicou em 2017 dados internos: ~16% dos testes deles eram flaky em algum grau. Custo estimado: bilhões de minutos de CPU por ano. A questão não é "se" você vai ter flaky tests, é "como" você vai gerenciá-los.

As causas comuns

Quase todo flaky test cai em uma destas categorias:

Tempo

Teste que assume "operação terminou em 100ms". Na maioria das vezes sim; ocasionalmente, em CI carregado, leva 200ms — falha. Sleeps hardcoded são o grande vilão. Thread.Sleep(100), time.sleep(0.1), time.Sleep(100 * time.Millisecond) espalhados pela suíte.

Solução: nunca espere por tempo absoluto. Use polling com timeout claro:

// Antipattern
service.SendAsync(message);
Thread.Sleep(500);   // espera "tempo suficiente"
queue.Count.Should().Be(1);

// Solução
service.SendAsync(message);
await Eventually(() => queue.Count == 1, timeout: 5.Seconds());

// Onde Eventually é util compartilhada que faz polling até
// condição ser true ou timeout.

Bibliotecas que ajudam: Awaitility (Java), polling2 (Python), em C# pode-se usar TaskCompletionSource ou utility custom. A ideia: aguarde o evento, não o relógio.

Concorrência mal-controlada

Múltiplas threads/goroutines sem sincronização adequada. Race condition em código de teste, ou no código sob teste, faz comportamento depender de scheduling — não-determinístico.

Diagnóstico: ferramentas de race detection (go test -race, ThreadSanitizer). Se elas reclamam, é a causa quase certa.

Estado compartilhado entre testes

Teste A insere usuário com email fixo "test@example.com". Teste B espera não encontrar esse email mas A já rodou e o deixou no banco. Ordem dos testes muda, comportamento muda.

Solução: cada teste começa do zero. Banco em transação rolledback, banco em container fresco, banco com schema reset. Em testes unitários, fakes resetados em setup_method.

Dependências externas

Teste chama API externa real. API está fora do ar 0.1% do tempo; seu teste falha 0.1% das vezes. Stack overflow tem dezenas de perguntas "por que minha CI falha intermitentemente?" cuja resposta é "você está chamando GitHub API real".

Solução: nenhum teste de unidade ou integração deveria depender de serviço externo real. Use VCR (gravar e replay), wiremock, ou fakes.

Ordem dos elementos

assert result == [a, b, c] mas o código retorna em ordem não-determinística (set, dict em Python < 3.7, map em Go). Funciona até a versão da linguagem ou hardware mudar e a ordem ser diferente.

Solução: ordene antes de comparar, ou compare como conjunto: assert sorted(result) == sorted([a,b,c]).

Recursos finitos

Teste depende de porta TCP fixa. Outra coisa no CI usa a porta. Falha. Use porta zero (kernel atribui livre) e leia qual foi atribuída.

Como diagnosticar um flaky

Quando flaky aparece, o instinto é "rodar mais vezes pra ver se reproduz". Funciona, mas é lento. Estratégia melhor:

  1. Rode em loop local: for i in {1..100}; do pytest test_xxx; done. Se reproduzir em < 100, ok; se não, suspeite que é dependência de ambiente CI específico.
  2. Adicione logging temporário: estado de variáveis relevantes, timing de operações. Próxima ocorrência vai ter contexto.
  3. Rode com race detector / sanitizers. Em Go, -race. Em C, -fsanitize=thread.
  4. Quarentena: marque como flaky e remova da suíte principal enquanto investiga. Não deixe ele continuar contaminando feedback do time.
  5. Investigue ou delete. Cada flaky é débito. Investigue até causa raiz (e corrija), ou delete se o teste não for crítico. Não deixe pendurado para sempre.
armadilha cultural

"Vamos só dar retry automático em testes flaky." Solução popular, ruim. Esconde causa raiz, normaliza não-determinismo, e quando bug real intermitente aparecer (race condition em produção manifestando às vezes em testes), você não vai notar — vai parecer outro flaky e ser rerunado. Retry automático é morfina, não antibiótico.

Política do time — recomendação

Times maduros adotam algo como:

Fechando o módulo

Este conceito completa o Módulo 02. Você passou por TDD em profundidade, estratégias de teste (pirâmide, troféu, honeycomb), vocabulário preciso de test doubles, BDD, property-based, mutation testing, contract testing, e os tópicos práticos de hoje. Esse conjunto é o que separa quem "tem testes" de quem "usa testes como ferramenta".

A ideia conectada: testes não são cerimônia que vem depois. São o mecanismo pelo qual qualidade vira propriedade estrutural do código, e pelo qual confiança em mudanças vira possível. Quando o time inteiro internaliza isso, refactorings agressivos viram seguros, deploys frequentes viram naturais, e bugs em produção viram raros — não porque ninguém erra, mas porque os erros são pegos antes.

Próximo módulo: Bancos de Dados. Lá vamos descer para o componente que sustenta praticamente todo sistema real — modelagem relacional, transações, índices, ACID, e o que muda em bancos não-relacionais. Sem entendimento profundo de banco, muita coisa de arquitetura mais para frente fica fora do alcance.

Como praticar

  1. Adote builders no projeto. Pegue uma classe de domínio crítica. Implemente FooBuilder. Refatore 10 testes para usar o builder. Note como o teste fica focado no que importa — só os campos que afetam o comportamento testado aparecem.
  2. Investigue um flaky real. Se você não tem um, crie: introduza um sleep aleatório, ou um ordering dependente em algum teste. Veja o teste falhar intermitentemente. Aplique o playbook de diagnóstico até identificar a causa.
  3. Experimente snapshot tests. Configure Verify (C#), syrupy (Python) ou cupaloy (Go) num caso onde verifica assertion manual seria doloroso (geração de e-mail, HTML render). Sinta a diferença. Faça o exercício de scrubbing de timestamps.

Referências para aprofundar

  1. livro xUnit Test Patterns — Gerard Meszaros (2007). Capítulos sobre Test Data Builders, Object Mother, e Test Setup. Catálogo canônico desses padrões.
  2. livro Growing Object-Oriented Software, Guided by Tests — Freeman & Pryce. Capítulos sobre construção de fixtures testáveis. Patterns de Test Data Builder. Onde o termo "Object Mother" é refinado.
  3. artigo ObjectMother — Easing Test Object Creation — Peter Schuh (XP 2001). Paper original que cunhou o nome. Curto, ilustrativo. Ainda é referência.
  4. artigo Test Data Builders: An Alternative to the Object Mother Pattern — Nat Pryce. natpryce.com — texto canônico que estabeleceu o padrão Builder em testes.
  5. artigo Where Do Our Flaky Tests Come From? — Lam, Godefroid, Nath, Santhiar, Thummalapenta (Microsoft). Paper de 2020 com análise empírica de causas raiz de flakies em codebase real. Categorias coincidem com as listadas aqui.
  6. artigo Flaky Tests at Google and How We Mitigate Them — Micco (2017). testing.googleblog.com — números brutos sobre escala do problema. Estratégias de detecção e quarentena automática.
  7. artigo Eradicating Non-Determinism in Tests — Martin Fowler. martinfowler.com/articles/nonDeterminism.html — taxonomia clara das causas de flakiness e estratégias por categoria.
  8. artigo Effective Snapshot Testing — Kent C. Dodds. kentcdodds.com/blog/effective-snapshot-testing — heurísticas práticas para usar snapshot sem cair nas armadilhas.
  9. docs Verify (.NET). github.com/VerifyTests/Verify — biblioteca moderna de snapshot testing em .NET. Documentação extensa, scrubbers built-in.
  10. docs syrupy (Python). github.com/syrupy-project/syrupy — snapshot tests para pytest. Boa alternativa ao pytest-snapshot.
  11. docs cupaloy (Go). github.com/bradleyjkemp/cupaloy — snapshot tests em Go. API simples, bem-documentada.
  12. vídeo The Building Blocks of Reliable Tests — Tatiana Pesotskaya. YouTube. Tratamento prático sobre fixtures, builders, e isolamento. Aplicável em qualquer linguagem.