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:
- JavaScript/React: Jest tornou snapshots populares para testar componentes UI. Praticamente padrão.
-
C# / .NET:
Verifyde Simon Cropp é moderno e poderoso. Salva snapshots em arquivos.verified.txt; primeira execução cria.received.txt; aprovação é renomear. -
Python:
pytest-snapshot,syrupy. Comportamento similar ao Jest. -
Go:
cupaloy,go-snaps. Padrão similar — primeira run salva, próximas comparam.
Quando snapshot brilha
- Outputs grandes e estruturados: HTML, JSON, código gerado. Verificar tudo manualmente é doloroso; snapshot captura a estrutura inteira.
- Detectar mudanças não-intencionais: você refatora um template; algum espaço em branco extra apareceu. Snapshot pega; assertion manual não pegaria.
-
Documentação viva: o arquivo
.verified.jsonmostra o output esperado. Para quem ler depois, é exemplo concreto.
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.
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:
- Tem defaults sensatos para tudo — caso comum é "
new OrderBuilder().Build()". - Permite sobrescrever só o que importa para o teste.
- Lê como prosa: "ordem com item de 100 enviado para RJ".
- Quando o domínio muda, só o builder muda — testes seguem funcionando.
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:
- C#: AutoFixture (gera valores arbitrários para qualquer tipo), Bogus (faker tradicional).
- Python: Faker, factory_boy (combina builders + Faker).
- Go: gofakeit, faker-go.
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ê?
- Destrói confiança. Quando vermelho às vezes significa "bug real" e às vezes "rerun da CI", time perde a capacidade de tratar vermelho como sinal sério.
- Encoraja "rerun até passar". Cultura informal de "deu vermelho? roda de novo até verde". Bugs reais intermitentes ficam mascarados.
- Custa tempo de CI. Cada rerun é minutos perdidos. Em times com dezenas de PRs por dia, soma horas.
- Espalha. Um teste flaky raramente fica isolado. Quem trabalhou nele sabe que é flaky e move pra frente; outros copiam o padrão.
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:
-
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. - Adicione logging temporário: estado de variáveis relevantes, timing de operações. Próxima ocorrência vai ter contexto.
-
Rode com race detector / sanitizers. Em Go,
-race. Em C,-fsanitize=thread. - Quarentena: marque como flaky e remova da suíte principal enquanto investiga. Não deixe ele continuar contaminando feedback do time.
- 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.
"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:
- Vermelho na main = alarme. Quando suíte falha em main, alguém investiga. Se for flaky conhecido, marca-se como tal e cria-se ticket. Não é normalizado.
- Flaky test em quarentena por < 1 sprint. Permitido. Mais que isso, decisão consciente: investigar seriamente ou deletar.
- Ranking de flaky por frequência. Pipelines modernos (CircleCI, GitHub Actions) trackeiam flakiness por teste. Top 10 flakies do trimestre vira tarefa de saneamento.
-
Testes que dependem de tempo morrem rápido.
Code review:
Thread.Sleepem teste é sinal vermelho automático.
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
-
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. - 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.
-
Experimente snapshot tests. Configure
Verify(C#),syrupy(Python) oucupaloy(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
- livro xUnit Test Patterns — Gerard Meszaros (2007).
- livro Growing Object-Oriented Software, Guided by Tests — Freeman & Pryce.
- artigo ObjectMother — Easing Test Object Creation — Peter Schuh (XP 2001).
- artigo Test Data Builders: An Alternative to the Object Mother Pattern — Nat Pryce.
- artigo Where Do Our Flaky Tests Come From? — Lam, Godefroid, Nath, Santhiar, Thummalapenta (Microsoft).
- artigo Flaky Tests at Google and How We Mitigate Them — Micco (2017).
- artigo Eradicating Non-Determinism in Tests — Martin Fowler.
- artigo Effective Snapshot Testing — Kent C. Dodds.
- docs Verify (.NET).
- docs syrupy (Python).
- docs cupaloy (Go).
- vídeo The Building Blocks of Reliable Tests — Tatiana Pesotskaya.