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.
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
- Confiança para mudar: o valor nº 1. Uma suíte verde depois de um refactor te diz "não quebrei nada óbvio". Sem isso, a base de código "congela" — ninguém mexe com medo.
- Documentação executável: um bom teste mostra como usar a unidade e o que ela garante — e não fica desatualizado (se ficar, quebra).
- Feedback de design: código difícil de testar quase sempre é código mal desenhado (acoplado demais, fazendo coisas demais). O teste é um "primeiro cliente" que reclama.
- Rede de detecção: pegar a regressão no CI, não no cliente.
- O que testes NÃO fazem: não provam ausência de bugs ("testes mostram a presença, não a ausência" — Dijkstra); não substituem revisão de código nem tipos; e não valem nada se ninguém os roda ou se são flaky.
1.2 Os tipos de teste
| Tipo | Escopo | Velocidade | Pega |
|---|---|---|---|
| Unitário | Uma "unidade" (função, classe, módulo) isolada | Milissegundos | Lógica, casos de borda, regras |
| Integração | Vários componentes juntos, geralmente com uma dependência real (banco, fila) | Centenas de ms a segundos | Contratos internos, SQL, serialização, config |
| Contrato | A interface entre dois serviços (consumidor ↔ provedor) | Rápido | Quebra de API entre times sem precisar rodar os dois juntos |
| End-to-end (E2E) | O sistema inteiro pela interface do usuário | Segundos a minutos | Fluxos críticos completos, integração real, "cola" entre tudo |
| Não-funcionais | Performance, carga, acessibilidade, segurança, resiliência (Módulo 7) | Varia | Comportamento 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)
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.
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. E2E — um 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.
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
| Propriedade | Significado |
|---|---|
| Fast | Milissegundos. Uma suíte lenta não é rodada |
| Isolated / Independent | Não depende da ordem, de outro teste, nem de estado compartilhado. Roda sozinho e em qualquer ordem |
| Repeatable | Determinístico: mesmo resultado sempre, em qualquer máquina. Sem depender de data/hora real, rede, aleatoriedade não semeada |
| Self-validating | Passa ou falha, sem inspeção humana do output |
| Timely | Escrito 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"); });
- Um comportamento por teste (pode ter várias asserções sobre o mesmo resultado).
- Nome que documenta: "faz X quando Y" — quem lê o relatório de falha entende sem abrir o código.
- Sem lógica no teste (if/for/try) — se o teste precisa de lógica, ele pode ter bugs; prefira table-driven / parametrizado.
- Object mothers / builders (
umCarrinho({...})) para montar dados de teste legíveis, com defaults sensatos e só o relevante explícito.
2.3 Test doubles
| Double | O que é | Quando |
|---|---|---|
| Dummy | Objeto passado só para preencher um parâmetro; nunca usado | Satisfazer uma assinatura |
| Stub | Retorna respostas prontas para as chamadas do teste | Controlar o que uma dependência devolve ("o gateway retorna erro") |
| Fake | Implementação real mas simplificada (repositório em memória, in-memory DB) | Substituir infra sem perder comportamento real |
| Spy | Stub que também registra como foi chamado | Verificar que um efeito colateral aconteceu (e-mail enviado) |
| Mock | Objeto com expectativas pré-programadas de chamadas; falha se não forem satisfeitas | Verificar interação com uma dependência (com moderaçã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
- Tempo: injetar um "clock" (ou usar fake timers) — nunca
Date.now()real num teste. - Aleatoriedade: semear o RNG, ou injetar a fonte de aleatoriedade.
- Ordem de iteração (maps/sets), fusos, locale — fixar.
- Rede / sistema de arquivos: substituir por fakes; se precisar de rede, é teste de integração, não unitário.
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 resultado — const 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.
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) ◄────────┘
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" / Classicist | London / Mockist / Outside-in | |
|---|---|---|
| Foco | Estado: testa o resultado, usa objetos reais/fakes, mocka só o necessário | Interação: testa colaborações, mocka os colaboradores, desce da UI para dentro |
| Prós | Testes robustos a refactor; menos frágeis | Feedback de design mais forte; TDD "de fora para dentro" guia a arquitetura |
| Contras | Pode adiar decisões de colaboração | Muitos 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
- Test-first (TDD): o teste guia o design; você não escreve código "não pedido"; menos over-engineering.
- Test-after: aceitável e comum; o risco é escrever testes que "confirmam o que o código já faz" (inclusive os bugs) e testar a implementação em vez do comportamento.
- Meio-termo útil: escrever o teste logo depois de um pequeno trecho, ainda com a intenção fresca, e usá-lo para dirigir o próximo trecho.
3.4 Quando TDD ajuda e quando atrapalha
| Ajuda | Atrapalha / não vale a pena |
|---|---|
| Lógica de negócio, regras, algoritmos, parsers — onde o "certo" é claro e há muitos casos | Exploraçã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 conserte | UI muito visual / layout — teste de snapshot ou visual, não TDD unitário |
| APIs cujo contrato você quer desenhar com cuidado | Có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).
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.
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"); });
- Cada suíte (ou cada teste, se rápido) com um contêiner limpo, ou
TRUNCATEentre testes — isolamento. - Mais lento que unit (segundos), então: rodar em paralelo, e num estágio de CI separado do
test:unitrápido.
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:
- Consumer-driven contracts (Pact): o consumidor define o que espera da API do provedor (nas suas requisições/respostas) e gera um contrato; o provedor roda esse contrato contra a sua implementação real no CI. Se o provedor quebrar a API, o build dele falha — antes de o consumidor perceber em produção.
- Schema-based: o provedor publica um OpenAPI/Protobuf; ferramentas (Schemathesis, Dredd, Pactflow,
oasdiff) validam que a implementação bate com o schema e que mudanças não são breaking (ver apostila de API Design). - Onde fica: entre o teste de integração e o E2E. Barato, rápido, e resolve o problema real ("os dois times evoluem sem falar e a API quebra").
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.
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
- Cobre: os fluxos de negócio críticos ponta a ponta (login, checkout, criar-e-publicar), integração real de todas as peças, e a "cola" que os testes menores não veem.
- Não cobre: casos de borda (isso é unit), validações de formulário exaustivas, cada permutação — colocá-las no E2E gera uma suíte lenta e frágil.
- Quantidade: dezenas, não milhares. Cada E2E deve poder ser justificado por "se este quebrar, é um incidente".
5.2 Playwright
Playwright (Microsoft) virou o padrão de E2E web moderno. Diferenciais:
| Recurso | O que resolve |
|---|---|
| Auto-waiting | Antes 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 assertions | await expect(locator).toBeVisible() re-tenta até um timeout — não "checa uma vez e falha" |
| Locators | getByRole, getByText, getByLabel, getByTestId — resilientes e alinhados à acessibilidade (ver apostila de a11y); evitar seletores CSS/XPath frágeis |
| Auto-isolamento | Cada teste roda num browser context novo (cookies/storage limpos) — testes independentes por padrão |
| Fixtures | Setup/teardown componível e tipado (usuário logado, dados semeados, página aberta) — sem beforeEach global bagunçado |
| Trace Viewer | Grava 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 sharding | Roda testes em paralelo por padrão; --shard distribui entre máquinas de CI |
| Multi-browser | Chromium, 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.
| Causa | Combate |
|---|---|
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 testes | Cada teste cria seus próprios dados (via API/seed) e limpa; isolamento por context |
| Dependência de rede externa real | Mockar rotas externas (page.route) ou usar um sandbox estável |
| Ordem e concorrência | Testes independentes; cuidado com estado global no backend de teste |
| Animações / tempo de render | Desabilitar 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
- Playwright: multi-browser real (inclui WebKit), auto-wait robusto, paralelização nativa, trace viewer, API testing, multi-linguagem (TS/JS, Python, .NET, Java). O default recomendado hoje.
- Cypress: ótima DX e time-travel debugger; historicamente limitado a Chromium-family e a um domínio por teste (melhorou); roda no browser.
- Selenium / WebDriver: o padrão histórico (WebDriver é um padrão W3C); mais verboso, sem auto-wait embutido, mas amplíssimo suporte de linguagem/browser e ecossistema (Grid). Ainda comum em empresas grandes.
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.
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); })); });
- Propriedades típicas: round-trip (encode/decode), invariantes (ordenar não muda o tamanho nem o conjunto de elementos), idempotência (
f(f(x)) === f(x)), comutatividade/associatividade, comparação com um oráculo (uma implementação lenta mas óbvia). - Shrinking: quando encontra uma falha, a ferramenta reduz o caso ao mínimo que ainda quebra ("falha com
[1, -2147483648]" em vez de um array de 300 números) — o bug fica óbvio. - Ferramentas: Hypothesis (Python), fast-check (JS/TS), jqwik (Java), proptest/quickcheck (Rust), PropEr/StreamData (Erlang/Elixir).
- Ideal para: parsers, serialização, algoritmos, lógica com muitos casos, matemática, validação.
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.
- Mutation score = % de mutantes mortos. É um indicador de qualidade da suíte muito melhor que cobertura de linha.
- Ferramentas: Stryker (JS/TS/C#/Scala), PIT/pitest (Java), mutmut/cosmic-ray (Python), go-mutesting (Go).
- É caro (roda a suíte N vezes) — rodar sobre o diff de um PR ou periodicamente, não a cada commit.
6.4 Snapshot e approval testing (com cuidado)
- Snapshot testing: a primeira execução grava a saída ("snapshot"); execuções seguintes comparam. Ótimo para saídas grandes e estáveis (um objeto complexo, uma resposta de API). Perigo: quando o snapshot quebra, é tentador só "atualizar" sem olhar — e você acabou de gravar o bug como "esperado". Regra: snapshots pequenos e legíveis, revisados a sério na atualização, e nunca de coisas que mudam legitimamente toda hora.
- Approval / golden testing: versão mais deliberada — a saída "aprovada" fica num arquivo versionado; a mudança é um diff explícito no PR que alguém aprova. Bom para geradores de código, formatadores, relatórios.
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.
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
| Tipo | Pergunta |
|---|---|
| Load test | Aguenta a carga esperada (X req/s) dentro do SLO de latência? |
| Stress test | Qual o ponto de quebra? Como degrada — graciosamente ou cai? |
| Spike test | Sobrevive a um pico súbito (10× em 1 min)? |
| Soak / endurance | Roda 24h sem vazar memória / degradar (leaks, conexões não fechadas)? |
| Benchmark de micro | Esta função ficou mais lenta desde a última versão? (no CI, com limiar) |
- Ferramentas: k6 (scripts em JS, ótimo para CI), Gatling (Scala/Java DSL), Locust (Python), JMeter (clássico), Vegeta.
- Sempre medir percentis (p95/p99), não média; definir o critério de aprovação como um threshold (p95 < 400ms E taxa de erro < 1%) — o teste falha o build se estourar.
- Testar contra um ambiente parecido com produção (dados, escala) — carga num ambiente de brinquedo não diz nada.
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
- SAST (análise estática — Semgrep, CodeQL, SonarQube): padrões de código inseguro.
- SCA / dependências (Dependabot, Snyk,
npm audit, OWASP Dependency-Check,govulncheck): CVEs em libs. - DAST (OWASP ZAP): ataca a app rodando.
- Secret scanning (gitleaks, trufflehog): credencial commitada.
- Testes de autorização negativa: um usuário tentando acessar o recurso de outro deve dar 403 (ver apostila de API Design — BOLA).
- Detalhe na apostila de Cibersegurança Prática & DevSecOps e na de Segurança de Aplicações de IA.
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.
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).
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
| Uso | Onde ajuda | O risco |
|---|---|---|
| Gerar testes a partir do código (Copilot, geradores) | Casos de borda óbvios, boilerplate de AAA, table-driven, parametrização | Testa 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/requisito | Traduzir critérios de aceitação em cenários | Interpretaçã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 agente | Agente navega a app e reporta comportamentos estranhos / gera casos | Não determinístico; difícil de reproduzir; precisa de curadoria humana |
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):
- Separe o que é determinístico: a maior parte do sistema (parsing, roteamento, chamada de ferramenta, formatação, guardrails, retries) é testável de forma clássica. Isole a chamada ao modelo atrás de uma interface e teste o resto com um stub.
- Assertions parciais / baseadas em propriedade: em vez de "a resposta é exatamente X", verifique "a resposta contém o número da política", "é JSON válido no schema", "não menciona a concorrência", "cita uma fonte", "tem menos de N tokens". São asserções determinísticas sobre uma saída não determinística.
- Datasets de avaliação (evals) + LLM-as-judge: um conjunto de casos com rubricas, julgados por um modelo calibrado contra humanos, rodando no CI como gate ("o score agregado não caiu > 2 pontos vs baseline"). Isso é a suíte de "testes" de um sistema de IA.
- Repetição para lidar com variância: rodar cada caso N vezes e exigir maioria; olhar o score agregado (estável) em vez de exigir 100% caso a caso; temperatura 0 nos testes.
- Testes adversariais: prompt injection, jailbreak, PII na saída (ver apostila de Segurança de Aplicações de IA).
- Regressão de modelo do provedor: a mesma suíte de evals rodando periodicamente pega quando o modelo por trás da API mudou e regrediu — algo que teste de código nunca pega.
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.
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.
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
- Injeção de dependências e ports & adapters / hexagonal (ver apostila de Arquitetura): o domínio puro testa sem infra; as fronteiras (banco, rede, relógio, filesystem) são interfaces substituíveis.
- Funções puras onde possível — entrada → saída, sem efeito colateral — são triviais de testar.
- Separar decisão de ação ("functional core, imperative shell"): a lógica que decide é pura e testada exaustivamente; a casca que faz (I/O) é fina e testada por integração.
- Ganchos de teste explícitos:
data-testidno frontend, endpoints de seed/reset num ambiente de teste, feature flags. - Se algo é difícil de testar, a resposta quase nunca é "escreva um teste feio" — é "refatore para ser testável".
9.2 Gestão de dados de teste
| Estratégia | Nota |
|---|---|
| Builders / object mothers / factories | Criar dados legíveis com defaults sensatos (umUsuario({ premium: true })) — a base |
| Cada teste cria e limpa os seus | Isolamento; via API/seed no setup, TRUNCATE/rollback no teardown |
| Transação por teste com rollback | Rápido para testes de repositório; cuidado com código que abre transação própria |
| Fixtures compartilhadas somente-leitura | Ok para dados de referência estáveis (países, moedas) |
| Anti-padrão: um "dump de produção" compartilhado | Drifta, contém PII, acopla testes entre si, e ninguém sabe o que pode mexer |
| Dados sintéticos / anonimizados | Para volume realista sem PII (cruza com governança — apostila de EU AI Act) |
9.3 Gestão de testes flaky
- Detecção: rodar a suíte (ou os testes alterados) N vezes; um teste com resultado inconsistente é marcado. Ferramentas de CI modernas (e serviços como CircleCI/BuildKite/Datadog CI Visibility) fazem isso.
- Quarentena: o teste flaky é removido do gate (não bloqueia o merge) mas continua rodando e reportando, com um dono e um prazo. Não é "delete e esquece".
- Dashboard de flakiness: taxa por teste, tendência, top ofensores. Trata-se como bug de prioridade.
retries: mascaram a flakiness. Aceitáveis em E2E com visibilidade (o relatório mostra "passou na 2ª tentativa"), nunca como forma de "resolver" — um teste que só passa no retry esconde um bug (na app ou no teste).- Orçamento de flakiness: acima de X% a suíte perde a confiança do time; priorizar a redução como se fosse um incidente.
9.4 Shift-left e shift-right
- Shift-left: testar cedo — tipos e lint na IDE, testes no commit, contract tests no PR, segurança no CI. Quanto mais cedo o bug é pego, mais barato.
- Shift-right / testing in production: algumas verificações só são reais em produção — canary (comparar SLIs da nova versão vs baseline), feature flags (ligar para 1% e observar), synthetic monitoring (rodar um E2E contra produção a cada minuto), A/B e observabilidade como "teste contínuo". Não substitui shift-left — complementa. Ver apostila de Observabilidade & SRE.
9.5 CI de testes em escala
- Estágios por velocidade:
lint + typecheck + unit(segundos, em todo push) →integração(minutos, testcontainers) →E2E(paralelizado/sharded) → não-funcionais (nightly ou pré-deploy). - Paralelização e sharding (Playwright
--shard, Jest/Vitest--shard, split por tempo histórico). - Test impact analysis / seleção de testes: rodar só os testes afetados pelo diff (Nx, Bazel, Vitest
--changed, ferramentas comerciais) — corta o tempo de CI drasticamente em monorepos. - Cache de dependências, de build, de imagens Docker.
- Fail fast vs run all: fail-fast no PR (feedback rápido); run-all no merge para o relatório completo.
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.
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?").
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
| Perfil | Foco |
|---|---|
| QA Engineer / Analyst | Estratégia de teste, casos, testes manuais exploratórios + automação |
| SDET / Test Automation Engineer | Frameworks de automação, Playwright/API/CI, dados de teste, flakiness |
| Quality Engineer / Software Engineer in Test | Qualidade como responsabilidade de engenharia; testabilidade, TDD, tooling |
| Performance / Reliability Engineer | Load testing, capacity, cruza com SRE |
| Todo backend/frontend dev | Testes são parte do "definition of done" — cai em toda entrevista de dev |
10.2 Roadmap de estudo (7–9 semanas)
| Semanas | Foco | Prática |
|---|---|---|
| 1 | Fundamentos: valor, pirâmide/trophy, tipos, comportamento vs implementação (Módulo 1) | Analisar uma suíte existente e classificar cada teste |
| 2 | Unit 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 |
| 3 | TDD: red-green-refactor num projeto real; escolas (Módulo 3) | Implementar uma feature inteira por TDD; corrigir 2 bugs "teste primeiro" |
| 4 | Integração + contrato: Testcontainers, Pact/Schemathesis (Módulo 4) | Teste de repositório com Postgres real; um contrato consumer-driven |
| 5 | Playwright: 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 |
| 6 | Property-based + mutation testing (Módulo 6) | fast-check/Hypothesis num parser; Stryker/PIT sobre um módulo e subir o score |
| 7 | Não-funcionais: k6, axe, regressão visual (Módulo 7) | Load test com threshold no CI; axe em todas as páginas |
| 8 | IA 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 |
| 9 | Consolidação, escrever uma test strategy, portfólio | Publicar 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
- 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. - 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.
- Property-based num parser/algoritmo: as propriedades, o shrinking encontrando um caso mínimo, e o bug que ele revelou.
- Anti-flakiness: pegar uma suíte E2E flaky (real ou plantada), documentar as causas e as correções, com o "antes/depois" da taxa.
- 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
- Fundamentos: Unit Testing Principles, Practices, and Patterns (Vladimir Khorikov) — a referência sobre o que faz um bom teste; Growing Object-Oriented Software, Guided by Tests (Freeman & Pryce) — o clássico de TDD outside-in; Working Effectively with Legacy Code (Michael Feathers).
- Prático: a documentação do Playwright (excelente); os artigos de Kent C. Dodds (testing trophy, "testing library"); o material de Testcontainers e de Pact.
- Estratégia: os escritos de Martin Fowler sobre testing (Test Pyramid, "Mocks Aren't Stubs", "Unit Test"), e o "Google Testing Blog".
- Property-based: a doc da Hypothesis (mesmo que você use outra linguagem — a filosofia está lá).
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?".