TL;DR: refizemos do zero a infra de testes E2E (Playwright + Docker + seeds determinísticos + contas por role) e ela virou o harness que permite usar AI no desenvolvimento sem medo: 1h de UAT manual virou 5-10 min, QA roda sozinho no preview de cada PR, e bugs de permissão invisíveis vieram à tona. Aqui está o como e o porquê, passo a passo.
Índice
- O problema: a AI acelerou tudo, menos o QA
- A tese: testes são o harness da AI
- A infra E2E: DB efêmero, seeds determinísticos e uma conta por role
- O pipeline de CI, do zero
- Fechando o loop: QA automatizado no preview do PR
- Resultados: o que isso vira em produto
- Takeaways
- Um obrigado — e por que resolvi escrever isto
O problema: a AI acelerou tudo, menos o QA
Com AI no fluxo de desenvolvimento, passamos a produzir muito mais código — e isso expôs um gargalo clássico: review e QA não acompanharam a velocidade.
No nosso caso (uma plataforma SaaS multi-tenant, time pequeno), isso significava:
- UATs manuais checando fluxo por fluxo, tirando print de tudo pra gerar evidência no PR
- DB local com estado "zoado" quebrando os testes E2E — e às vezes o próprio app
- Uma única conta de teste pra tudo, escondendo bugs de permissão
- PRs parados esperando alguém ter 1 hora livre pra validar
Cada UAT manual custava ~1 hora. Hoje custa 5-10 minutos instruindo uma AI. Mas o ponto não é economizar tempo com o esporte — é o que você faz com o tempo de volta. Cada hora que não gasto clicando na tela manualmente é uma hora construindo produto: uma feature a mais, um fluxo mais bem pensado, uma dívida técnica paga. Esse post é sobre o que foi construído pra chegar aí — e por que isso é uma decisão de produto, não só de engenharia.
A tese: testes são o harness da AI
"Harness", no contexto de coding com AI, é tudo aquilo que garante que a AI vai fazer a coisa certa: o ambiente, os guardrails, os loops de feedback. A gente enxerga os testes exatamente assim.
O raciocínio central é este: se os testes unit cobrem os casos da lógica, os E2E cobrem todos os fluxos do usuário, e o código gerado passou no review seguindo os padrões do projeto — o que sobra pra você "vasculhar" manualmente?
Sempre sobra algo — exceções, detalhes de UX. Mas o grosso da confiança vem do harness. E isso muda a relação com a AI: em vez de revisar cada linha com desconfiança, você deixa o harness fazer o trabalho pesado e foca sua atenção onde ela vale mais.
Sem esse harness, o cenário se inverte: a AI gera código mais rápido do que um humano consegue revisar, e a velocidade vira passiva — bugs entram na mesma velocidade que as features, regressões silenciosas passam despercebidas até o cliente reclamar, e cada mudança da AI exige UAT manual de novo, de volta à estaca zero.
E aqui a coisa deixa de ser técnica e vira produto: um harness sólido é o que permite ao time se mover rápido sem medo. Quando você confia que os fluxos críticos estão cobertos, aceita mudanças maiores, refatora sem pânico, entrega mais. Quando não confia, cada deploy é uma aposta — e o custo disso não aparece no board, aparece no churn, no suporte, na confiança do usuário que quebra silenciosamente. Qualidade não é o oposto de velocidade; é o que torna a velocidade sustentável.
A infra E2E: DB efêmero, seeds determinísticos e uma conta por role
No final do ano passado, fiquei responsável por refazer a infra e os testes E2E do zero. Minha abordagem foi incremental: primeiro o básico funcionando, depois estudar os padrões, e refatorar aplicando boas práticas o quanto antes.
DB efêmero com Docker: o teste é dono do banco
O maior problema era o estado do banco — a solução foi tirar esse estado da equação. Ao rodar pnpm test:e2e:ui, o global setup do Playwright sobe um MySQL isolado, roda migrations e seeds, e tira um snapshot restaurado entre arquivos de teste:
// tests/e2e/global-setup.ts (simplificado)
export default async function globalSetup() {
await dbManager.startContainer(); // MySQL 8 dedicado via Docker (porta própria, DB isolado)
await dbManager.waitForMySql(); // espera o healthcheck
await dbManager.runMigrations();
await dbManager.runSeeds(); // demo-data + seed-data de teste
await dbManager.createSnapshot(); // snapshot: restaurado entre arquivos de teste
if (!(await dbManager.validateTestUser())) {
throw new Error('Test user not found after seeding!');
}
}
O resultado é sempre idêntico, local e no CI. Até o MySQL do Compose tem tuning pra teste (innodb-flush-log-at-trx-commit=0, buffer pool ajustado): durabilidade não importa num banco descartável, velocidade sim. E como o banco é restaurado entre arquivos de teste, um spec não contamina o outro.
Uma conta por role: o detalhe que mais pagou
Testar tudo com uma única conta admin escondia exatamente os bugs de permissão que mais importavam — porque admin passa em qualquer tela. A correção foi criar um provisionamento de contas cobrindo todas as roles do sistema, definidas numa única fonte de verdade tipada:
// tests/e2e/setup/test-credentials.ts (trecho)
export type E2EAccountKey =
| 'owner'
| 'admin'
| 'main-supervisor'
| 'supervisor'
| 'therapist'
| 'therapist-assistant'
| 'relative';
export const E2E_ACCOUNTS: Record<E2EAccountKey, E2EAccountDefinition> = {
supervisor: {
email: 'supervisor@example.com',
memberRole: MemberRoles.MEMBER,
staffProfession: StaffProfessionEnum.SUPERVISOR,
assignmentAttribution: StaffAttributionEnum.SUPERVISOR,
},
// ...uma entrada por role, seedada no setup e logada via storageState
};
Assim que cada teste passou a usar a role certa, começaram a aparecer bugs de access denied que ninguém estava percebendo — porque nenhum fluxo era exercitado com as permissões reais do usuário final. Se você tem RBAC e testa tudo com admin, seus testes de permissão não existem.
Esse foi, disparado, o investimento com melhor retorno da infra toda — e não por acaso o mais ligado ao produto: bug de permissão não é bug técnico qualquer, é um usuário vendo dado que não devia, ou travado fora de algo que era pra ele. É exatamente o tipo de coisa que corrói confiança e é caríssima de descobrir em produção.
POM + BDD: o formato que a AI entende
Os testes seguem Page Object Model, organizados por domínio (pages/child, pages/relatives, pages/session...), com fixtures e test-data builders. E opcionalmente escrevemos .feature files (Gherkin/BDD) descrevendo os fluxos — isso virou documentação executável, e é aqui que a AI brilha: com fluxos documentados, POMs consistentes e o Playwright MCP disponível, a AI gera specs E2E novos seguindo os padrões do projeto, em vez de inventar seletores frágeis do zero. Antes dessa estrutura existir, gerar E2E com AI simplesmente não funcionava direito.
Um exemplo real, de um fluxo crítico que envolve duas roles no mesmo teste: o supervisor convida um responsável, e o responsável abre o convite no próprio dispositivo e preenche a anamnese (ficha clínica de entrada) da criança:
Scenario: Invited relative completes every editable anamnese field
Given a main supervisor is logged in
And the supervisor invited and assigned a relative to the seeded child
And the relative completed registration from the invitation on their device
When the relative completes every editable anamnese field
Then the draft is autosaved and remains complete after reload
When the relative submits the anamnese
Then the submitted answers remain complete for the relative
And the main supervisor sees the same complete anamnese
E o spec segue o cenário passo a passo — cada test.step espelha uma linha do Gherkin, então o report do Playwright lê como o fluxo de negócio:
// testIsolated: restaura o snapshot do DB antes de cada teste (inclusive retries)
test.use({ restoreDb: undefined });
test.use({ storageState: 'playwright/.auth/main-supervisor.json' }); // role certa, já logada
test('Invited relative completes every editable anamnese field', async ({ browser, page }) => {
// Contexto incógnito separado = o relative no "próprio dispositivo",
// sem derrubar a sessão do supervisor
const relativeContext = await browser.newContext({ storageState: { cookies: [], origins: [] } });
const relativePage = await relativeContext.newPage();
const registration = new InviteRegistrationPage(relativePage); // POMs por domínio
const relativeAnamnese = new AnamnesePage(relativePage);
await test.step('Given a main supervisor invited and assigned a relative to the seeded child', async () => {
await relatives.createRelative('en', { childId, email: relativeEmail, name: relativeName });
});
await test.step('And the relative completed registration from the invitation on their device', async () => {
const invitation = await db.invitation.findFirstOrThrow({ where: { email: relativeEmail } });
await registration.register({ invitationId: invitation.id, email: relativeEmail, password });
});
// ...When/Then: preenche todos os campos, valida autosave após reload,
// submete, e confere que o SUPERVISOR vê a mesma anamnese completa
});
Repare nos detalhes que o harness carrega: a role entra pronta via storageState, o segundo login vive num browser context isolado (multiusuário real, sem gambiarras de logout), e o testIsolated restaura o snapshot do banco antes de cada teste — então esse fluxo inteiro de convite + registro + formulário roda quantas vezes for preciso, sempre a partir do mesmo estado.
O pipeline de CI, do zero
A regra é simples: barato e rápido roda primeiro, caro roda só se o resto já passou. Quando um PR sai de draft, o workflow roda em dois jobs encadeados:
Job 1 — Quality Checks (gate rápido, ~15 min de timeout):
- Quality checks (lint, typecheck, etc.)
- Testes unit
- Testes de integração
Job 2 — E2E (só roda se o Job 1 passar):
- Sobe MySQL 8 via Docker num DB isolado (
proaba_e2e_test) - Gera um
.envde teste (secrets fake,NODE_ENV=test) - Instala o Chromium do Playwright
- Roda o global setup (healthcheck → migrations → seeds → snapshot → validação)
- Executa a suíte E2E completa
- Faz upload do report do Playwright como artifact (mesmo em falha)
Como o banco nasce e morre dentro do job, zero dependência de ambiente compartilhado — o clássico "passa local, quebra no CI" praticamente sumiu.
Fechando o loop: QA automatizado no preview do PR
Com a base pronta, o core dev do projeto montou em cima um fluxo de pr-qa: cada PR gera um preview deployment, e quando a URL fica disponível, um agente valida o fluxo de ponta a ponta sozinho — e entrega um veredito de merge baseado em evidência, não em achismo.
Na prática, o agente:
- Lê o diff do PR e mapeia o "blast radius" — quais telas/fluxos renderizam o código alterado
- Loga no preview e navega os fluxos afetados em todos os locales (pt-br e en)
- Para toda mutação no diff (form, POST/PATCH/DELETE): preenche, submete, valida o toast de sucesso, recarrega e confirma que persistiu — e depois reverte. Uma tela que renderiza mas não salva é bug silencioso de perda de dados
- Roda um analyzer estático de regressão de i18n (chaves removidas mas ainda usadas) + scan do DOM procurando raw keys
- Captura screenshots de tudo
E o PR template fecha o ciclo do outro lado: ele tem uma persona embutida pra AI que escreve a descrição, com honesty rules explícitas — não pode afirmar que testes/UAT passaram sem incluir output, link de CI ou screenshot; se não consegue verificar, deixa um TODO(HUMAN). Um check no CI valida que o template foi preenchido. Ou seja: até o "a AI disse que funciona" precisa de evidência.
Resultados: o que isso vira em produto
Em números diretos:
- 1 hora de UAT manual → 5-10 minutos instruindo a AI, que roda os fluxos e captura evidências sozinha
- Ciclo PR aberto → review → merge muito mais rápido; muitas horas economizadas por semana
- Bugs de permissão que estavam invisíveis apareceram assim que as roles certas entraram nos testes
- Um efeito composto: quanto mais testes entram, maior a chance de pegar o próximo bug antes do usuário
Mas os números só importam pelo que destravam. As horas que voltam não somem numa planilha de produtividade — elas viram capacidade de construir: mais features entregues, mais tempo pra ouvir o usuário e pensar em UX, menos gente parada apagando incêndio de regressão. A qualidade que o harness garante é o que deixa a equipe entregar rápido, continuando a confiar no que entrega. Num time pequeno, essa é a diferença entre estar sempre correndo atrás e conseguir de fato empurrar o produto pra frente. No fim, testar bem é uma alavanca de produto disfarçada de disciplina de engenharia.
Takeaways
- Testes são o harness da AI. Sem eles, a velocidade da AI vira passivo; com eles, vira velocidade de verdade.
- O teste tem que ser dono do banco. DB efêmero + seeds determinísticos + snapshot mata a maior fonte de flakiness.
- Teste com as roles reais. Uma conta admin pra tudo esconde exatamente os bugs mais graves.
- Estrutura primeiro, AI depois. POM, fixtures e fluxos documentados são o que fazem a geração de teste por AI funcionar — não o contrário.
- Evidência > afirmação. QA automatizado com screenshots e mutation testing, e PR descriptions que não podem alegar sucesso sem prova.
Um obrigado — e por que resolvi escrever isto
Confesso que escrever sobre o próprio trabalho não é natural pra mim. O empurrão veio de um livro pequeno do Austin Kleon, o Show Your Work!. A ideia que me pegou foi a de compartilhar o processo, não só o resultado polido — e a de que ensinar o que você aprendeu não subtrai valor do seu trabalho, pelo contrário: agrega. Foi exatamente com esse espírito que documentei aqui não o "temos testes E2E", mas o como e o porquê de cada decisão.
Então, se algo aqui te poupar algumas das horas que eu gastei descobrindo na marra — sobre banco efêmero, contas por role ou usar testes como harness de AI — já valeu. Obrigado, Kleon, pelo empurrão. E se você tem um harness melhor, ou discorda de alguma escolha, me conta nos comentários: sigo aprendendo em público. 🙂
Top comments (1)
I like the E2E-as-harness framing. The big win is not only faster QA, it is giving the AI workflow a repeatable contract: here is the behavior that must still be true after generation.