Testes

Gestão de dados de teste: o gargalo invisível da automação

Pergunte a qualquer pessoa que mantém uma suíte de automação qual a maior causa de teste instável, e a resposta raramente é "a lógica do teste". Quase sempre é dado: o usuário que outro teste apagou, o cupom que já foi usado, o registro que existia ontem e hoje não. Escrever a asserção é a parte fácil; garantir que o sistema está no estado certo antes dela rodar é o trabalho de verdade — e o mais negligenciado.

Por que dado é tão difícil quanto a lógica

Um teste só é confiável se o estado em que ele depende for previsível. O problema é que dado tem três inimigos naturais: ele muda (alguém alterou o registro), ele some (foi apagado), e ele colide (dois testes mexem no mesmo registro ao mesmo tempo). Quando um teste verde de repente fica vermelho "sem ninguém ter mexido em nada", o culpado quase sempre é o dado — e isso é uma das maiores fontes de testes flaky.

De onde vem o dado: as quatro estratégias

Há quatro formas de colocar o sistema no estado necessário, da mais frágil à mais robusta:

EstratégiaComo éProblema / quando usar
Dado de produçãoCópia (mascarada) do banco realRealista, mas instável e arriscado (LGPD); evite depender
Seed compartilhadoMassa fixa carregada uma vez para todosCômodo, mas vira "estado global" que os testes corrompem entre si
FixtureConjunto pré-definido por teste/cenárioBom para dados de leitura estáticos
Factory (sob demanda)O teste cria o que precisa na hora, com dados únicosA mais robusta para dados que o teste modifica

A tendência moderna é clara: menos seed global, mais factory. Em vez de "esses 50 usuários existem para todo mundo", cada teste cria o seu próprio usuário no início, com um identificador único. Ninguém pisa no pé de ninguém.

A regra de ouro: cada teste cuida do próprio estado

O princípio que resolve a maior parte dos problemas: um teste deve criar o que precisa, usar, e limpar depois — sem depender de nada que outro teste tenha feito. Isso garante a independência que permite rodar os testes em qualquer ordem e em paralelo.

// Padrão arrange → act → assert → cleanup
beforeEach: usuario = criarUsuario({ email: `teste+${uuid()}@ex.com` })
test:       login(usuario); comprar(produto)
            expect(pedido).toBe("confirmado")
afterEach:  removerUsuario(usuario)   // não deixa lixo para trás

O uuid() no e-mail é o detalhe que evita colisão: mesmo rodando 10 cópias do teste em paralelo, cada uma tem seu usuário único. E o cleanup no afterEach impede que a falha de hoje vire o dado órfão que quebra o teste de amanhã.

Um teste que depende de um dado que ele não criou é uma bomba-relógio. Funciona até o dia em que aquele registro muda — e aí você perde uma tarde caçando um "bug" que nunca esteve no código.

Criar pela API, não pela interface

Para preparar o estado, prefira a rota mais barata e estável. Se o seu teste de checkout precisa de um usuário com produtos no carrinho, não automatize o cadastro e a navegação inteira pela tela só para chegar lá — crie o usuário e o carrinho direto por chamadas de API (ou direto no banco) no setup, e reserve a interface para o que você de fato quer testar. É mais rápido, e a preparação não quebra quando a tela de cadastro muda. Setup pesado pela UI é uma das receitas mais comuns de suíte lenta e frágil.

Como manter o ambiente limpo

Mesmo com cada teste limpando o seu, o banco de teste acumula sujeira com o tempo. Três abordagens, da mais forte à mais prática:

  • Transação revertida: roda o teste dentro de uma transação e dá rollback no fim. Limpeza perfeita e instantânea — mas só funciona em testes que batem no mesmo banco diretamente, não atravessando a rede.
  • Reset entre execuções: recriar o schema ou recarregar um snapshot limpo antes da suíte. Garante um ponto de partida conhecido a cada rodada.
  • Cleanup explícito: o afterEach/afterAll apaga o que criou. Mais trabalhoso, mas é o que funciona em testes E2E de ponta a ponta.

Containers efêmeros (subir um banco em Docker só para a rodada do pipeline e destruir no fim) combinam o melhor dos mundos: ambiente isolado, limpo por construção, descartado ao terminar.

Conclusão

Dado de teste é o gargalo invisível: ele não aparece na asserção, mas é onde a maioria das suítes apodrece. Prefira factories sob demanda a seed global, faça cada teste criar e limpar o próprio estado com identificadores únicos, prepare o cenário pela API em vez da tela, e mantenha o ambiente limpo com transação, reset ou containers efêmeros. Resolva o dado e você elimina a maior fatia dos testes flaky de uma vez.

Ask anyone who maintains an automation suite what the biggest cause of unstable tests is, and the answer is rarely "the test logic". It's almost always data: the user another test deleted, the coupon that was already used, the record that existed yesterday and is gone today. Writing the assertion is the easy part; making sure the system is in the right state before it runs is the real work, and the most neglected.

Why data is as hard as logic

A test is only reliable if the state it depends on is predictable. The problem is that data has three natural enemies: it changes (someone edited the record), it disappears (it was deleted), and it collides (two tests touch the same record at the same time). When a green test suddenly turns red "without anyone touching anything", the culprit is almost always the data, and this is one of the biggest sources of flaky tests.

Where data comes from: the four strategies

There are four ways to put the system in the required state, from most fragile to most robust:

StrategyWhat it looks likeProblem / when to use
Production data(Masked) copy of the real databaseRealistic, but unstable and risky (privacy laws); avoid depending on it
Shared seedFixed data loaded once for everyoneConvenient, but becomes "global state" that tests corrupt for each other
FixturePredefined set per test/scenarioGood for static read-only data
Factory (on demand)The test creates what it needs on the spot, with unique dataThe most robust for data the test modifies

The modern trend is clear: less global seed, more factory. Instead of "these 50 users exist for everyone", each test creates its own user at the start, with a unique identifier. Nobody steps on anyone's toes.

The golden rule: each test takes care of its own state

The principle that solves most problems: a test should create what it needs, use it, and clean up afterwards, without depending on anything another test did. This guarantees the independence that lets you run tests in any order and in parallel.

// Pattern: arrange → act → assert → cleanup
beforeEach: user = createUser({ email: `test+${uuid()}@ex.com` })
test:       login(user); buy(product)
            expect(order).toBe("confirmed")
afterEach:  deleteUser(user)   // leaves no trash behind

The uuid() in the email is the detail that avoids collisions: even running 10 copies of the test in parallel, each one has its own unique user. And the cleanup in afterEach keeps today's failure from becoming the orphaned data that breaks tomorrow's test.

A test that depends on data it didn't create is a time bomb. It works until the day that record changes, and then you lose an afternoon chasing a "bug" that was never in the code.

Create through the API, not the UI

To prepare state, prefer the cheapest and most stable route. If your checkout test needs a user with products in the cart, don't automate the whole sign-up and navigation through the screen just to get there. Create the user and the cart directly through API calls (or straight in the database) in the setup, and reserve the UI for what you actually want to test. It's faster, and the preparation doesn't break when the sign-up screen changes. Heavy setup through the UI is one of the most common recipes for a slow, brittle suite.

How to keep the environment clean

Even with each test cleaning up after itself, the test database accumulates dirt over time. Three approaches, from strongest to most practical:

  • Rolled-back transaction: run the test inside a transaction and rollback at the end. Perfect, instant cleanup, but it only works for tests that hit the same database directly, not across the network.
  • Reset between runs: recreate the schema or reload a clean snapshot before the suite. Guarantees a known starting point on every run.
  • Explicit cleanup: afterEach/afterAll deletes what it created. More work, but it's what works in end-to-end E2E tests.

Ephemeral containers (spinning up a Docker database just for a pipeline run and destroying it at the end) combine the best of both worlds: an isolated environment, clean by construction, discarded when done.

Conclusion

Test data is the invisible bottleneck: it doesn't show up in the assertion, but it's where most suites rot. Prefer on-demand factories to a global seed, make each test create and clean up its own state with unique identifiers, prepare the scenario through the API instead of the UI, and keep the environment clean with transactions, resets, or ephemeral containers. Solve the data and you eliminate the biggest slice of flaky tests in one go.

Leia também

Voltar ao blog