Testes não provam que o código funciona — dão coragem para mudá-lo

Apostila completa de Testing Moderno & Automação

Uma suíte de testes boa é a diferença entre um código que a equipe tem medo de tocar e um que se refatora à vontade. Esta apostila vai do teste unitário bem escrito ao TDD como técnica de design, aos testes de contrato e integração com dependências reais, ao Playwright para end-to-end, e às fronteiras — property-based, mutation testing, e o duplo desafio da IA: usá-la para escrever testes e testar sistemas que contêm IA.

10 módulosTDD · test doubles · pirâmide/trophyPlaywright · contract testing · testcontainersproperty-based · mutation testingBoxes de entrevistaExercícios com gabarito
MÓDULO 01 · BÁSICO

Por que testar e a pirâmide

Objetivo: entender o que uma suíte de testes realmente compra, os tipos de teste, e como distribuí-los (a pirâmide clássica e o "trophy" moderno).

1.1 O que os testes compram

1.2 Os tipos de teste

TipoEscopoVelocidadePega
UnitárioUma "unidade" (função, classe, módulo) isoladaMilissegundosLógica, casos de borda, regras
IntegraçãoVários componentes juntos, geralmente com uma dependência real (banco, fila)Centenas de ms a segundosContratos internos, SQL, serialização, config
ContratoA interface entre dois serviços (consumidor ↔ provedor)RápidoQuebra de API entre times sem precisar rodar os dois juntos
End-to-end (E2E)O sistema inteiro pela interface do usuárioSegundos a minutosFluxos críticos completos, integração real, "cola" entre tudo
Não-funcionaisPerformance, carga, acessibilidade, segurança, resiliência (Módulo 7)VariaComportamento sob condições, não "funcionalidade"

1.3 A pirâmide e o troféu

A PIRÂMIDE (Mike Cohn)              O "TESTING TROPHY" (Kent C. Dodds)

        /\   E2E  (poucos)                 ___
       /  \                               /E2E\      poucos
      /----\  Integração (alguns)        /-----\
     /      \                           / Integ.\    MUITOS  ← o centro de gravidade
    /--------\ Unit (muitos, rápidos)  /---------\
                                       |  Unit   |  alguns
                                       |_________|
                                       (+ análise estática / tipos na base)
💡 A ideia central de ambos

Tenha muitos testes rápidos e baratos (unitários / integração de baixo nível) e poucos testes lentos e caros (E2E). O E2E dá a maior confiança por teste, mas é lento, frágil e caro de manter — use-o para os fluxos de negócio críticos, não para cobrir cada caso de borda (isso é trabalho de unitário). O "trophy" moderno desloca o peso para testes de integração ("teste como o usuário usa, no maior nível que ainda seja rápido e confiável") e coloca tipos/lint na fundação. O anti-padrão é a "cone de sorvete": muitos E2E, poucos unitários — CI lento, flaky e frustrante.

1.4 O que "testar como o usuário usa" significa

Um princípio que unifica testing moderno (frontend e backend): teste comportamento observável, não detalhes de implementação. "Ao clicar em Comprar com o carrinho vazio, aparece a mensagem X" > "o método validateCart chama isEmpty". O primeiro sobrevive a refactors; o segundo quebra a cada mudança interna e não prova nada de útil.

💼 Mercado de trabalho

Perguntas de abertura: "Qual o valor de uma suíte de testes?" (confiança para mudar, doc executável, feedback de design — não "provar que funciona"), "Explique a pirâmide de testes e o anti-padrão do cone de sorvete", "O que é testar comportamento vs implementação e por que importa?". QA/SDET e "Quality Engineering" são áreas com muita vaga e escassez de gente boa.

✏️ Exercício 1 — Distribua os testes

Um app de e-commerce. Você tem orçamento para ~200 testes. Como distribuiria entre unit / integração / E2E, e o que colocaria em cada camada para a função "aplicar cupom de desconto"?

Gabarito (exemplo): Distribuição ~ 140 unit / 45 integração / 15 E2E. Para "aplicar cupom": unit — todos os casos de borda da regra (cupom expirado, valor mínimo não atingido, cupom já usado, percentual vs fixo, cupom sobre frete, empilhamento proibido, arredondamento) — dezenas de testes rápidos. Integração — o serviço de cupom com o banco real (testcontainers): cupom lido do BD, contador de uso incrementado, concorrência de dois usos simultâneos. E2Eum teste: usuário adiciona item, aplica um cupom válido, vê o total correto e finaliza. Não replicar os 20 casos de borda no E2E.

MÓDULO 02 · BÁSICO

Testes unitários bem feitos

Objetivo: as propriedades de um bom teste (FIRST), a estrutura AAA, o vocabulário de test doubles, e o erro de mockar demais.

2.1 FIRST

PropriedadeSignificado
FastMilissegundos. Uma suíte lenta não é rodada
Isolated / IndependentNão depende da ordem, de outro teste, nem de estado compartilhado. Roda sozinho e em qualquer ordem
RepeatableDeterminístico: mesmo resultado sempre, em qualquer máquina. Sem depender de data/hora real, rede, aleatoriedade não semeada
Self-validatingPassa ou falha, sem inspeção humana do output
TimelyEscrito junto (ou antes) do código de produção, não meses depois

2.2 Estrutura: AAA / Given-When-Then

test("cupom expirado não aplica desconto", () => {
  // ARRANGE (given): monta o cenário
  const carrinho = umCarrinho({ total: 100 });
  const cupom = umCupom({ percentual: 10, expiraEm: ontem() });

  // ACT (when): executa a ação sob teste
  const resultado = aplicarCupom(carrinho, cupom);

  // ASSERT (then): verifica o resultado observável
  expect(resultado.total).toBe(100);
  expect(resultado.erro).toBe("cupom_expirado");
});

2.3 Test doubles

DoubleO que éQuando
DummyObjeto passado só para preencher um parâmetro; nunca usadoSatisfazer uma assinatura
StubRetorna respostas prontas para as chamadas do testeControlar o que uma dependência devolve ("o gateway retorna erro")
FakeImplementação real mas simplificada (repositório em memória, in-memory DB)Substituir infra sem perder comportamento real
SpyStub que também registra como foi chamadoVerificar que um efeito colateral aconteceu (e-mail enviado)
MockObjeto com expectativas pré-programadas de chamadas; falha se não forem satisfeitasVerificar interação com uma dependência (com moderação)
⚠️ Mockar demais acopla o teste à implementação

Um teste cheio de expect(mock.metodoX).toHaveBeenCalledWith(...) não verifica comportamento — verifica como o código faz. Renomeie um método interno ou reordene chamadas e o teste quebra sem que nada esteja errado (falso negativo), e um bug real pode passar porque o mock "confirmou" a chamada esperada (falso positivo). Prefira: fakes a mocks; mockar só as fronteiras do sistema (rede, relógio, sistema de arquivos, serviços externos), não os colaboradores internos; e assertar sobre o resultado, não sobre as interações.

2.4 Determinismo

💼 Mercado de trabalho

Perguntas: "O que é FIRST?", "Diferença entre stub, mock, fake e spy", "Por que mockar demais é ruim?" (acopla à implementação; falsos positivos e negativos), "O que você mockaria e o que não?" (fronteiras do sistema sim; colaboradores internos, prefira o real ou um fake), "Como tornar um teste que usa data/hora determinístico?" (injetar clock).

✏️ Exercício 2 — Conserte o teste

Um teste de criarPedido() tem: expect(repo.save).toHaveBeenCalledTimes(1), expect(emailService.send).toHaveBeenCalled(), expect(logger.info).toHaveBeenCalledWith("pedido criado"), e usa new Date() para o campo criadoEm. Aponte os problemas e reescreva a ideia.

Gabarito: Problemas: (1) testa interações internas (repo.save, logger.info) — acoplado à implementação; renomear/refatorar quebra sem motivo. (2) logger.info com string exata é ainda pior (mensagem de log não é comportamento). (3) new Date() torna o teste não determinístico. Reescrever: usar um repositório fake em memória e assertar sobre o resultadoconst pedido = await criarPedido(dados); expect(pedido.status).toBe("pendente"); expect(await repo.findById(pedido.id)).toEqual(pedido);. Para o e-mail (efeito de fronteira), um spy e assertar o essencial: expect(emailFake.enviados).toContainEqual({ para: dados.email, template: "confirmacao" }). Injetar um clock: criarPedido(dados, { now: () => FIXED }) e assertar pedido.criadoEm === FIXED.

MÓDULO 03 · INTERMEDIÁRIO

TDD: red-green-refactor como design

Objetivo: o ciclo do TDD, por que ele é uma técnica de design (não só de teste), as duas "escolas", e quando ajuda ou atrapalha.

3.1 O ciclo

 ┌──────────────────────────────────────────────────┐
 │  1. RED    — escreva um teste que falha           │
 │             (define o comportamento que você quer) │
 │  2. GREEN  — escreva o MÍNIMO de código para passar│
 │             (nem elegante, nem geral — só passar)  │
 │  3. REFACTOR — melhore o design com a rede verde   │
 │             (sem mudar comportamento; testes cobrem)│
 └──────────────► repita (ciclos de minutos) ◄────────┘
💡 TDD é sobre design, não sobre cobertura

Escrever o teste primeiro força você a decidir a interface antes da implementação — como a unidade será chamada, o que retorna, como sinaliza erro — do ponto de vista de quem a usa. Isso tende a produzir APIs mais simples e código mais desacoplado (porque código acoplado é difícil de testar, e você sente essa dor imediatamente). A alta cobertura é um efeito colateral, não o objetivo.

3.2 As duas escolas

Chicago / "Detroit" / ClassicistLondon / Mockist / Outside-in
FocoEstado: testa o resultado, usa objetos reais/fakes, mocka só o necessárioInteração: testa colaborações, mocka os colaboradores, desce da UI para dentro
PrósTestes robustos a refactor; menos frágeisFeedback de design mais forte; TDD "de fora para dentro" guia a arquitetura
ContrasPode adiar decisões de colaboraçãoMuitos mocks → testes frágeis se exagerado

Na prática, a maioria dos times usa uma mistura pragmática: outside-in para explorar o design de uma feature, classicist para a maior parte dos testes de unidade (mais estáveis).

3.3 Test-first × test-after

3.4 Quando TDD ajuda e quando atrapalha

AjudaAtrapalha / não vale a pena
Lógica de negócio, regras, algoritmos, parsers — onde o "certo" é claro e há muitos casosExploração / spike — você ainda não sabe o que quer; testar cedo trava a exploração
Corrigir um bug: escreva o teste que reproduz, depois conserteUI muito visual / layout — teste de snapshot ou visual, não TDD unitário
APIs cujo contrato você quer desenhar com cuidadoCódigo throwaway / protótipo descartável
Refatoração de legado (escrever "testes de caracterização" primeiro)Integrações que só fazem sentido com a dependência real (vá direto ao teste de integração)

3.5 O mito da cobertura de 100%

Cobertura de linha mede o que foi executado, não o que foi verificado. Você pode ter 100% de cobertura com testes que não têm asserção nenhuma. É útil como detector de partes não testadas (cobertura baixa é sinal de alerta), mas péssimo como meta (leva a testes-teatro que inflam o número). Melhor sinal de qualidade da suíte: mutation testing (Módulo 6) e o escape rate de bugs (Módulo 9).

💼 Mercado de trabalho

Perguntas: "Descreva o ciclo do TDD" (red-green-refactor; escrever o mínimo), "Por que dizem que TDD é uma técnica de design?", "London vs Chicago school", "Quando você NÃO usaria TDD?" (spike, UI visual, throwaway), "100% de cobertura garante qualidade?" (não — mede execução, não verificação).

✏️ Exercício 3 — TDD de um FizzBuzz com regra extra

Descreva os primeiros 3 ciclos red-green-refactor para uma função fizzbuzz(n) que retorna "Fizz" para múltiplos de 3, "Buzz" para 5, "FizzBuzz" para 15, e o número como string caso contrário.

Gabarito: Ciclo 1 — RED: expect(fizzbuzz(1)).toBe("1") (falha, função não existe). GREEN: return String(n). REFACTOR: nada. Ciclo 2 — RED: expect(fizzbuzz(3)).toBe("Fizz") (falha, retorna "3"). GREEN: if (n % 3 === 0) return "Fizz"; return String(n). REFACTOR: nada. Ciclo 3 — RED: expect(fizzbuzz(5)).toBe("Buzz"). GREEN: adicionar if (n % 5 === 0) return "Buzz" antes do return. REFACTOR: nada ainda. Ciclo 4 — RED: expect(fizzbuzz(15)).toBe("FizzBuzz") (falha, retorna "Fizz"). GREEN: construir a string concatenando "Fizz" e "Buzz" e retornar String(n) se vazia. REFACTOR: extrair as regras num array [[3,"Fizz"],[5,"Buzz"]] e reduzir — agora com todos os testes verdes cobrindo. Cada teste força uma decisão; o código emerge mínimo.

MÓDULO 04 · INTERMEDIÁRIO

Integração e testes de contrato

Objetivo: testar com dependências reais sem lentidão nem flakiness (testcontainers), e garantir a compatibilidade entre serviços sem rodá-los todos juntos (contract testing).

4.1 O problema do "integrado mas mockado"

Se você mocka o banco, a fila e todo serviço externo, seus "testes de integração" testam a sua mocagem, não a integração. Bugs reais moram exatamente nas fronteiras: um JOIN errado, um mapeamento de coluna, um timezone, um formato de data, uma constraint, um limite de payload, o comportamento de "linha não encontrada". Mock nenhum pega isso.

4.2 Testcontainers

Testcontainers (bibliotecas para Java, Go, .NET, Node, Python…) sobe a dependência real em um contêiner Docker efêmero durante o teste — um Postgres de verdade, um Redis, um Kafka, um LocalStack para AWS — e o derruba ao fim. Você ganha realismo (o SQL roda no mesmo motor da produção) sem manter infra de teste compartilhada (que sempre driftou e ninguém confia).

// esboço (Node/Vitest + Testcontainers)
let pg;
beforeAll(async () => {
  pg = await new PostgreSqlContainer("postgres:16").start();
  await migrate(pg.getConnectionUri());
});
afterAll(() => pg.stop());

test("salva e recupera um pedido, preservando o timezone", async () => {
  const repo = new PedidoRepo(pg.getConnectionUri());
  const criado = await repo.save(umPedido({ criadoEm: "2026-08-30T14:00:00Z" }));
  const lido = await repo.findById(criado.id);
  expect(lido.criadoEm.toISOString()).toBe("2026-08-30T14:00:00.000Z");
});

4.3 Contract testing

Testar dois serviços rodando juntos em E2E é lento e frágil. Contract testing verifica a interface entre eles sem isso:

💼 Mercado de trabalho

Perguntas: "Por que 'testar a integração mockando tudo' não funciona?" (bugs moram nas fronteiras — SQL, mapeamento, timezone), "O que é Testcontainers e o que resolve?" (dependência real efêmera, sem infra compartilhada driftada), "O que é consumer-driven contract testing?" (o consumidor define o esperado; o provedor valida no CI), "Contract testing ou E2E entre serviços?" (contract é mais barato e rápido para pegar quebra de API).

✏️ Exercício 4 — Onde cada teste

Serviço de "Pagamentos" (consumidor) chama a API do serviço "Ledger" (provedor). Você quer garantir: (a) que o repositório do Pagamentos grava e lê corretamente do Postgres; (b) que se o Ledger mudar o nome de um campo da resposta, alguém percebe antes de produção; (c) que o fluxo "pagar um pedido" funciona ponta a ponta. Que tipo de teste para cada?

Gabarito: (a) Teste de integração com Testcontainers — Postgres real efêmero, testa o repositório de verdade. (b) Contract test (Pact) — Pagamentos gera o contrato do que espera da resposta do Ledger; o CI do Ledger roda esse contrato contra a implementação real e falha se o campo mudar. (c) Um teste E2E — o fluxo crítico "pagar um pedido" pela API/UI, com os serviços rodando (ou o Ledger stubbed pelo contrato); poucos desses.

MÓDULO 05 · INTERMEDIÁRIO

End-to-end e Playwright

Objetivo: o que E2E deve e não deve cobrir, o Playwright a fundo (auto-wait, locators, fixtures, trace viewer, paralelização), e como combater a flakiness.

5.1 O papel do E2E

5.2 Playwright

Playwright (Microsoft) virou o padrão de E2E web moderno. Diferenciais:

RecursoO que resolve
Auto-waitingAntes de clicar/preencher, espera o elemento estar acionável (visível, estável, habilitado). Mata a maior fonte de flakiness — os sleep() arbitrários
Web-first assertionsawait expect(locator).toBeVisible() re-tenta até um timeout — não "checa uma vez e falha"
LocatorsgetByRole, getByText, getByLabel, getByTestId — resilientes e alinhados à acessibilidade (ver apostila de a11y); evitar seletores CSS/XPath frágeis
Auto-isolamentoCada teste roda num browser context novo (cookies/storage limpos) — testes independentes por padrão
FixturesSetup/teardown componível e tipado (usuário logado, dados semeados, página aberta) — sem beforeEach global bagunçado
Trace ViewerGrava um trace completo (DOM, network, console, screenshots por passo) — abrir o trace de um teste que falhou no CI mostra exatamente o que aconteceu, sem reproduzir localmente
Paralelização e shardingRoda testes em paralelo por padrão; --shard distribui entre máquinas de CI
Multi-browserChromium, Firefox, WebKit; emulação mobile; API testing embutido (request)
test("usuário completa o checkout", async ({ page, loginComoCliente }) => {
  await loginComoCliente();                 // fixture
  await page.goto("/produtos/cafe-especial");
  await page.getByRole("button", { name: "Adicionar ao carrinho" }).click();
  await page.getByRole("link", { name: "Finalizar compra" }).click();
  await page.getByLabel("Cartão").fill("4242 4242 4242 4242");
  await page.getByRole("button", { name: "Pagar" }).click();
  await expect(page.getByText("Pedido confirmado")).toBeVisible();   // re-tenta até timeout
});

5.3 Flakiness — a doença do E2E

Um teste flaky passa e falha sem mudança de código. Um pouco de flakiness corrói toda a confiança na suíte ("ah, é flaky, roda de novo") e esconde bugs reais.

CausaCombate
Esperas fixas (sleep(2000))Auto-wait + web-first assertions; nunca sleep arbitrário
Seletores frágeis (classe CSS que muda)Locators por role/label/text/testid
Dados compartilhados entre testesCada teste cria seus próprios dados (via API/seed) e limpa; isolamento por context
Dependência de rede externa realMockar rotas externas (page.route) ou usar um sandbox estável
Ordem e concorrênciaTestes independentes; cuidado com estado global no backend de teste
Animações / tempo de renderDesabilitar animações; esperar por estado, não por tempo
Race no app (a app é que é flaky)O teste achou um bug real — conserte a app, não o teste

Gestão de flaky (Módulo 9): detecção automática (rodar N vezes), quarentena (isola do gate, mas continua rodando e reportando), dashboard de flakiness, e prazo para consertar ou deletar. retries mascaram — use com parcimônia e sempre com visibilidade.

5.4 Playwright × Cypress × Selenium

💼 Mercado de trabalho

Playwright é a skill mais pedida em vagas de automação hoje. Perguntas: "O que E2E deve e não deve cobrir?" (fluxos críticos; não casos de borda), "Como o Playwright reduz flakiness?" (auto-wait, web-first assertions, locators, isolamento por context), "Como você investiga um E2E que falhou só no CI?" (Trace Viewer), "Um teste flaky: o que você faz?" (quarentena + investigar; se a app é que corre, é bug real), "Playwright vs Cypress vs Selenium".

✏️ Exercício 5 — Estabilize o teste

Um E2E de checkout falha ~1 em 5 execuções no CI. O código tem: await page.waitForTimeout(3000) após clicar em "Pagar"; usa page.locator('.btn-success') para achar o botão; e depende de um cupom fixo "PROMO10" que outros testes também usam. Como você conserta?

Gabarito: (1) Remover o waitForTimeout(3000) e trocar por await expect(page.getByText("Pedido confirmado")).toBeVisible() — auto-retry até o timeout, sem chute de tempo. (2) Trocar .locator('.btn-success') por page.getByRole("button", { name: "Pagar" }) — resiliente a mudança de CSS. (3) O cupom compartilhado: cada teste cria o próprio cupom via API no setup (fixture) e usa um código único; ou o teste semeia seus dados e não depende de estado global. (4) Se ainda falhar, abrir o Trace Viewer da execução vermelha para ver o passo exato — pode ser uma race real na app (ex.: o botão fica clicável antes de o handler estar pronto), que aí se conserta na app.

MÓDULO 06 · AVANÇADO

Property-based, fuzzing e mutation testing

Objetivo: além dos testes por exemplo — gerar casos automaticamente (property-based, fuzzing) e medir a qualidade da suíte (mutation testing), sabendo os limites do snapshot.

6.1 Property-based testing

Em vez de escrever exemplos ("somar(2,3) === 5"), você declara propriedades que valem para qualquer entrada, e a ferramenta gera centenas de casos aleatórios (incluindo bordas: 0, negativos, muito grande, string vazia, unicode) tentando quebrar a propriedade.

// fast-check (JS) — propriedades de uma função de serialização
test("round-trip: parse(serialize(x)) === x", () => {
  fc.assert(fc.property(fc.record({ nome: fc.string(), idade: fc.nat() }), (x) => {
    expect(parse(serialize(x))).toEqual(x);
  }));
});

6.2 Fuzzing

Alimentar a função/programa com entradas malformadas, aleatórias ou geradas por cobertura em massa, procurando crashes, hangs, panics, vazamentos de memória. Diferença para property-based: fuzzing foca em robustez (não quebrar feio) mais que em correção, e roda por horas/contínuo. Ferramentas: libFuzzer/AFL++ (C/C++/Rust via cargo-fuzz), go test -fuzz (nativo em Go), Jazzer (JVM), Atheris (Python). Essencial para código que processa entrada não confiável (parsers de formato, protocolos, decoders — cruza com a apostila de Cibersegurança).

6.3 Mutation testing "quem testa os testes?"

Cobertura diz o que foi executado. Mutation testing diz o que foi verificado: a ferramenta introduz pequenos bugs no código de produção ("mutantes" — troca > por >=, + por -, remove uma linha, inverte um if) e roda a suíte. Se algum teste falha, o mutante foi "morto" (bom — a suíte pega esse tipo de erro). Se todos passam, o mutante "sobreviveu" — há um buraco na suíte naquele ponto.

6.4 Snapshot e approval testing (com cuidado)

💼 Mercado de trabalho

Perguntas (diferenciam sénior): "O que é property-based testing e quando usar?" (declarar propriedades, gerar casos, shrinking; parsers/algoritmos), "Cobertura de linha garante qualidade da suíte? O que garante melhor?" (mutation testing, escape rate), "O que é mutation testing e por que é caro?", "Riscos do snapshot testing" (atualizar sem revisar = gravar o bug).

✏️ Exercício 6 — Propriedades de um carrinho

Escreva (em linguagem natural) três propriedades property-based para uma função calcularTotal(itens, cupom) de um carrinho.

Gabarito (exemplos): (1) Monotonicidade: adicionar um item com preço ≥ 0 nunca diminui o total (sem cupom, ou com cupom que não vira o resultado negativo). (2) Limite inferior: o total nunca é negativo, para qualquer combinação de itens e qualquer cupom válido — um cupom de 90% num carrinho de R$ 5 dá no mínimo 0, não −R$ 4,50. (3) Cupom nulo é identidade: calcularTotal(itens, null) === calcularTotal(itens, cupomDe0Porcento) e ambos == soma dos preços. (4) Comutatividade da ordem dos itens: embaralhar a lista de itens não muda o total. (5) Idempotência do cupom: aplicar o mesmo cupom "de novo" (se a regra proíbe empilhar) não muda o total.

MÓDULO 07 · AVANÇADO

Testes não-funcionais

Objetivo: testar o que não é "funcionalidade" — performance e carga, acessibilidade, regressão visual, segurança no CI, resiliência — e onde cada um encaixa no pipeline.

7.1 Performance e carga

TipoPergunta
Load testAguenta a carga esperada (X req/s) dentro do SLO de latência?
Stress testQual o ponto de quebra? Como degrada — graciosamente ou cai?
Spike testSobrevive a um pico súbito (10× em 1 min)?
Soak / enduranceRoda 24h sem vazar memória / degradar (leaks, conexões não fechadas)?
Benchmark de microEsta função ficou mais lenta desde a última versão? (no CI, com limiar)

7.2 Acessibilidade automatizada

Ferramentas como axe-core (via @axe-core/playwright, jest-axe, Lighthouse) pegam automaticamente ~30–50% dos problemas de acessibilidade (contraste, alt ausente, labels de formulário, ordem de headings, ARIA inválido) — rode em cada página no CI. Os outros ~50–70% exigem teste manual e com tecnologia assistiva. Detalhe completo na apostila de Acessibilidade digital & WCAG.

7.3 Regressão visual

Captura screenshots de componentes/páginas e compara pixel a pixel (com tolerância) contra uma baseline aprovada — pega quebras de layout, CSS, e mudanças não intencionais que testes funcionais não veem. Ferramentas: Playwright toHaveScreenshot(), Percy, Chromatic (para Storybook), Applitools (com IA para reduzir falsos positivos). Cuidado com flakiness de renderização (fontes, antialiasing, animações — desabilitar) e com baselines que ninguém revisa.

7.4 Segurança no pipeline

7.5 Resiliência

Chaos engineering (injetar falhas: matar instâncias, adicionar latência, particionar rede) e game days validam que a resiliência projetada (timeouts, retries, circuit breakers, fallbacks — ver apostilas de Arquitetura e de SRE) realmente funciona antes do incidente real.

💼 Mercado de trabalho

Perguntas: "Load, stress, spike e soak test — a diferença", "Como você define o critério de aprovação de um teste de carga?" (threshold de p95 e taxa de erro; falha o build), "Quanto da acessibilidade dá para automatizar?" (~30–50%; o resto é manual), "O que a regressão visual pega que o teste funcional não pega?" (layout/CSS/quebras visuais). Ferramenta a citar: k6.

✏️ Exercício 7 — Monte o gate não-funcional

Um checkout novo vai para produção. Além dos testes funcionais, que verificações não-funcionais você exigiria no pipeline antes do deploy?

Gabarito (exemplo): (1) Load test (k6) contra staging com dados realistas: 2× o pico esperado, threshold p95 < 500ms e erro < 1% — falha o build se estourar. (2) Spike test: simular a rajada de uma campanha. (3) axe-core nas telas do fluxo (Playwright) — zero violações críticas/sérias. (4) Regressão visual das telas de checkout. (5) SCA + SAST + secret scan no CI, gate em vulnerabilidade alta/crítica. (6) Testes de autorização negativa nos endpoints de pagamento. (7) Opcional pré-lançamento: um game day simulando a queda do gateway de pagamento para validar o fallback. Deploy via canary com monitor de SLO (ver SRE).

MÓDULO 08 · AVANÇADO

Testes na era da IA

Objetivo: os dois desafios — usar IA para escrever e manter testes (com seus limites), e testar sistemas que contêm IA (não determinismo, evals).

8.1 IA para escrever e manter testes

UsoOnde ajudaO risco
Gerar testes a partir do código (Copilot, geradores)Casos de borda óbvios, boilerplate de AAA, table-driven, parametrizaçãoTesta o que o código faz (incluindo bugs), não o que deveria; testes tautológicos; falsa sensação de cobertura
Gerar testes a partir de spec/requisitoTraduzir critérios de aceitação em cenáriosInterpretação errada da spec; casos faltando
Manter / consertar testes (self-healing locators)Ajustar seletores quebrados por mudança de DOM"Consertar" um teste que deveria falhar (mascara regressão real)
Exploração assistida por agenteAgente navega a app e reporta comportamentos estranhos / gera casosNão determinístico; difícil de reproduzir; precisa de curadoria humana
⚠️ O anti-padrão: "testes que a IA escreveu e ninguém revisou"

Uma suíte gerada por IA e mergeada sem revisão crítica infla a cobertura e a confiança sem aumentar a proteção real. Muitos desses testes só re-afirmam a implementação atual (mutation score baixo), quebram a cada refactor legítimo, e não pegam os bugs que importam. Regra: IA gera o rascunho; o humano decide qual comportamento merece um teste, revisa cada asserção ("isto verifica algo que eu me importaria se quebrasse?"), e valida com mutation testing sobre o diff. IA acelera a digitação, não substitui o julgamento sobre o que testar.

8.2 Testar sistemas que contêm IA

Quando o sistema sob teste usa um LLM ou um modelo de ML, o teste tradicional (entrada → saída exata) quebra: a saída é texto aberto e não determinístico. As abordagens (detalhe completo na apostila de LLMOps & Avaliação):

💡 A regra que unifica

Para código de IA, você troca "o teste passou / falhou" (binário) por "a métrica de qualidade acompanhou ou regrediu" (contínuo, estatístico). A disciplina é a mesma dos testes clássicos — casos representativos, rodar no CI, bloquear regressão — mas a asserção é sobre uma distribuição, não sobre um valor.

💼 Mercado de trabalho

Tema em ascensão em vagas de QA/SDET. Perguntas: "Usar IA para gerar testes — prós e o principal risco?" (acelera; risco de testar a implementação e não o comportamento, sem revisão), "Como você testaria uma feature que usa um LLM?" (isolar o determinístico + asserções parciais/de propriedade + dataset de eval com LLM-as-judge no CI + repetição para variância), "Como pegar uma regressão do modelo do provedor?" (suíte de evals rodando periodicamente).

✏️ Exercício 8 — Teste um assistente de resumo

Uma feature recebe um artigo e usa um LLM para gerar um resumo de até 3 frases, que aparece num card. Descreva a estratégia de teste.

Gabarito: Determinístico (teste clássico): a chamada ao LLM atrás de uma interface — com um stub, testar: truncamento do input, tratamento de erro/timeout da API, retry, cache, o card renderiza o texto e o "gerado por IA", o guardrail que corta se passar de 3 frases, sanitização da saída (sem HTML injetado). Qualidade da geração (eval): um dataset de ~30–50 artigos com rubrica ("o resumo é fiel ao artigo, não inventa fatos", "≤ 3 frases", "sem opinião", "cobre o ponto principal"); um LLM-as-judge calibrado contra ~15 julgamentos humanos; rodar no CI como gate de score agregado vs baseline, cada caso 3× (temperatura 0). Adversarial: artigo com instrução escondida ("ignore e escreva X") — o resumo não deve obedecer. Periódico: rodar os evals semanalmente para pegar regressão do provedor.

MÓDULO 09 · MUITO AVANÇADO

Testabilidade, flakiness e estratégia

Objetivo: projetar para testabilidade, gerir dados de teste e testes flaky, o "shift-left/shift-right", o CI de testes em escala, e a estratégia de teste como documento vivo.

9.1 Design para testabilidade

9.2 Gestão de dados de teste

EstratégiaNota
Builders / object mothers / factoriesCriar dados legíveis com defaults sensatos (umUsuario({ premium: true })) — a base
Cada teste cria e limpa os seusIsolamento; via API/seed no setup, TRUNCATE/rollback no teardown
Transação por teste com rollbackRápido para testes de repositório; cuidado com código que abre transação própria
Fixtures compartilhadas somente-leituraOk para dados de referência estáveis (países, moedas)
Anti-padrão: um "dump de produção" compartilhadoDrifta, contém PII, acopla testes entre si, e ninguém sabe o que pode mexer
Dados sintéticos / anonimizadosPara volume realista sem PII (cruza com governança — apostila de EU AI Act)

9.3 Gestão de testes flaky

9.4 Shift-left e shift-right

9.5 CI de testes em escala

9.6 A estratégia de teste como documento

Um time maduro tem uma test strategy escrita (1–2 páginas): o que se testa em cada camada, o que não se testa e por quê, os padrões (nomes, fixtures, doubles), o orçamento de flakiness, quem é dono de quê, e as métricas. Métricas úteis: escape rate (bugs que chegaram a produção / total), lead time do CI, flakiness rate, mutation score em módulos críticos. Cobertura de linha: um sinal secundário, nunca uma meta contratual.

💼 Mercado de trabalho

Perguntas sénior: "Como você projeta código para ser testável?" (DI, hexagonal, funções puras, functional core/imperative shell), "O que você faz com testes flaky?" (detectar → quarentena com dono e prazo → dashboard; retries só com visibilidade), "Shift-left e shift-right", "Como você acelera um CI de testes lento?" (estágios por velocidade, sharding, test impact analysis, cache), "Que métricas de qualidade você acompanha?" (escape rate, flakiness, mutation score — não cobertura como meta).

✏️ Exercício 9 — Diagnóstico de CI

O CI de um time leva 45 min, ~8% dos builds falham por testes flaky (E2E), e bugs continuam escapando para produção apesar de 85% de cobertura. Proponha um plano.

Gabarito: Velocidade (45 min): separar estágios (unit em segundos no push; integração e E2E depois); paralelizar/shard os E2E; test impact analysis para rodar só o afetado no PR; cachear deps/build. Flakiness (8%): ligar detecção automática, quarentenar os flaky (fora do gate, mas rodando e reportando, com dono e prazo), dashboard de flakiness, e atacar as causas (esperas fixas → auto-wait, seletores frágeis → roles, dados compartilhados → isolamento). Meta < 1%. Bugs escapando apesar de 85% cobertura: a cobertura está enganando — rodar mutation testing nos módulos críticos para achar os buracos reais; revisar se os testes verificam comportamento ou implementação; acrescentar testes de integração/contrato nas fronteiras (onde os bugs provavelmente moram); medir escape rate e fazer postmortem leve de cada bug escapado ("que teste teria pego isto?").

MÓDULO 10 · CARREIRA

Mercado de trabalho: roadmap, entrevistas e portfólio

Objetivo: converter o conteúdo dos módulos anteriores em aprovação — perfis, roadmap, banco de perguntas com respostas e projetos que geram entrevista.

10.1 Os perfis que contratam

PerfilFoco
QA Engineer / AnalystEstratégia de teste, casos, testes manuais exploratórios + automação
SDET / Test Automation EngineerFrameworks de automação, Playwright/API/CI, dados de teste, flakiness
Quality Engineer / Software Engineer in TestQualidade como responsabilidade de engenharia; testabilidade, TDD, tooling
Performance / Reliability EngineerLoad testing, capacity, cruza com SRE
Todo backend/frontend devTestes são parte do "definition of done" — cai em toda entrevista de dev

10.2 Roadmap de estudo (7–9 semanas)

SemanasFocoPrática
1Fundamentos: valor, pirâmide/trophy, tipos, comportamento vs implementação (Módulo 1)Analisar uma suíte existente e classificar cada teste
2Unit bem feito: FIRST, AAA, test doubles, determinismo (Módulo 2)Escrever a suíte unitária de um módulo de regras de negócio, sem mockar demais
3TDD: red-green-refactor num projeto real; escolas (Módulo 3)Implementar uma feature inteira por TDD; corrigir 2 bugs "teste primeiro"
4Integração + contrato: Testcontainers, Pact/Schemathesis (Módulo 4)Teste de repositório com Postgres real; um contrato consumer-driven
5Playwright: locators, fixtures, trace viewer, paralelização, anti-flakiness (Módulo 5)Suíte E2E de 8–10 fluxos críticos de uma app; zero waitForTimeout
6Property-based + mutation testing (Módulo 6)fast-check/Hypothesis num parser; Stryker/PIT sobre um módulo e subir o score
7Não-funcionais: k6, axe, regressão visual (Módulo 7)Load test com threshold no CI; axe em todas as páginas
8IA nos testes + testabilidade + flakiness + CI em escala (Módulos 8–9)Um eval no CI para uma feature de LLM; um plano de aceleração de CI
9Consolidação, escrever uma test strategy, portfólioPublicar a strategy + o repositório de exemplo

10.3 Banco de perguntas de entrevista (com a resposta que aprova)

Júnior/pleno — "Explique a pirâmide de testes"

Muitos testes rápidos e baratos na base (unitários), menos no meio (integração), poucos e caros no topo (E2E). O E2E dá a maior confiança por teste mas é lento e frágil — reserve-o para fluxos de negócio críticos. O anti-padrão é o "cone de sorvete" (muitos E2E, poucos unit): CI lento e flaky. O "trophy" moderno desloca o peso para integração e coloca tipos/lint na fundação.

Pleno — "Diferença entre stub, mock e fake; o que você mockaria?"

Stub: retorna respostas prontas. Mock: tem expectativas de chamada e falha se não forem satisfeitas (verifica interação). Fake: implementação real simplificada (repo em memória). Eu mockaria as fronteiras do sistema — rede, relógio, filesystem, serviços externos — e usaria o real ou um fake para os colaboradores internos, assertando sobre o resultado, não sobre "foi chamado com X". Mockar demais acopla o teste à implementação: quebra em refactor legítimo e deixa passar bugs reais.

Pleno — "O que é TDD e por que 'design'?"

Red (teste que falha, define o comportamento) → green (mínimo para passar) → refactor (melhora o design com a rede verde). É design porque escrever o teste primeiro força a decidir a interface do ponto de vista de quem usa, antes de implementar — o que produz APIs mais simples e código mais desacoplado, já que código acoplado dói para testar imediatamente. Cobertura alta é efeito colateral.

Pleno/sénior — "Um E2E falha só no CI, passa localmente. O que você faz?"

Abrir o Trace Viewer da execução que falhou (Playwright grava DOM, network, console, screenshots por passo) — mostra o passo e o estado exatos sem reproduzir. Causas comuns: espera fixa em vez de auto-wait, seletor frágil, dado compartilhado com outro teste rodando em paralelo, timing de rede/render, ou um bug real de race na app (que se conserta na app, não no teste). Se for genuinamente instável e não bloqueante agora, quarentenar com dono e prazo — nunca só adicionar retry e esquecer.

Sénior — "Cobertura de 90% garante qualidade?"

Não. Cobertura mede o que foi executado, não o que foi verificado — dá para ter 90% com testes sem asserção ou que testam a implementação. Sinais melhores: mutation score (introduz bugs e vê se a suíte os pega) nos módulos críticos, e escape rate (bugs que chegaram a produção). Uso a cobertura só como detector de partes não testadas (cobertura baixa = alerta), nunca como meta.

Sénior — "Como testar uma feature que usa um LLM?"

Isolar a chamada ao modelo atrás de uma interface e testar todo o resto (parsing, roteamento, guardrails, retry, render) de forma clássica com um stub. Para a geração: asserções parciais/de propriedade sobre a saída não determinística ("é JSON no schema", "cita a fonte", "≤ 3 frases", "não menciona a concorrência"); um dataset de evals com rubricas julgado por LLM-as-judge calibrado, rodando no CI como gate de score agregado vs baseline, com repetição (N×, temperatura 0) para a variância; testes adversariais (injeção, PII). E rodar os evals periodicamente para pegar regressão do modelo do provedor.

Armadilha — "Vou pedir para a IA gerar todos os testes"

IA gera bem o boilerplate e casos de borda óbvios, mas testes gerados sem revisão tendem a re-afirmar a implementação atual (incluindo bugs), quebram a cada refactor e não pegam o que importa — inflam a cobertura sem aumentar a proteção real. O fluxo correto: IA escreve o rascunho; o humano decide qual comportamento merece teste, revisa cada asserção ("eu me importaria se isto quebrasse?"), e valida com mutation testing. A IA acelera a digitação, não o julgamento.

10.4 Projetos de portfólio que geram entrevista

  1. Suíte completa de um projeto real (âncora): uma app pequena com pirâmide bem distribuída — unit (sem mockar demais), integração com Testcontainers, um contract test, E2E com Playwright (fixtures, zero waitForTimeout, trace no CI), e um gate não-funcional (k6 + axe). README com a test strategy escrita.
  2. Estudo de mutation testing: pegar um módulo com "boa cobertura", rodar Stryker/PIT, mostrar os mutantes sobreviventes e os testes que você adicionou para matá-los.
  3. Property-based num parser/algoritmo: as propriedades, o shrinking encontrando um caso mínimo, e o bug que ele revelou.
  4. Anti-flakiness: pegar uma suíte E2E flaky (real ou plantada), documentar as causas e as correções, com o "antes/depois" da taxa.
  5. Eval de uma feature de IA: um dataset + LLM-as-judge + gate no CI para uma pequena feature que usa um LLM (cruza com a apostila de LLMOps).

10.5 Fontes para continuar

🏁 Síntese final da apostila

Quatro ideias sustentam testing moderno: (1) o valor é coragem para mudar — testes que quebram a cada refactor legítimo são passivo, não ativo; (2) teste comportamento observável, não implementação, e mocke só as fronteiras; (3) muitos testes rápidos, poucos lentos, e os lentos (E2E) só para os fluxos críticos, blindados contra flakiness; (4) cobertura não é qualidade — mutation score e escape rate dizem a verdade. E para IA: use-a para escrever testes com revisão, e para testar IA troque "passou/falhou" por "a métrica regrediu?".