🧠FFVAcademy
🧪

Testes Profissionais: pirâmide, propriedades, contrato e fuzz

18 min de leitura·+85 XP
Pré-requisitos (0/1)0%

Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.

Teste não é esporte. Não é rito. É sistema de garantia que permite mudar código sem medo. Em 2026, com agents escrevendo muito mais código do que humanos, o valor de testes bons subiu: eles são o contrato entre intenção humana e execução do agent. Teste bom hoje > cobertura alta. Este módulo mapeia os tipos que importam, onde cada um brilha e onde é cerimônia vazia.

A pirâmide moderna (Honeycomb/Diamante)

🗺️ Do barato/muitos ao caro/poucos
🔬Unit tests70-75%
Funções puras, lógica de negócio isolada. ms. Use mock com moderação.
sustenta
🔗Integração15-20%
DB real (Testcontainers), queue real, API real. Cobre o glue.
valida contrato
📜Contract tests5-8%
Pact/OpenAPI. Entre serviços. Rápidos, decisivos.
simula usuário
🌐E2E / UI2-5%
Playwright, Cypress. Golden paths apenas. Caro, flaky por natureza.
estressa
Chaos / Loadsetup separado
K6, Gatling, Chaos Mesh. Produção-like. Não no CI normal.
💡
Pirâmide vs Diamante.Times modernos inflam integração (“diamante”) porque DB em Docker/Testcontainers ficou barato e integração pega bugs reais que unit puro ignora. Regra: se seu código é 80% glue (chama outras APIs/DB), teste de integração é o caminho.

Unit: rápido, puro, opinativo

typescript
// src/money.ts
export function splitBill(total: number, people: number): number[] {
  if (people <= 0) throw new Error('people must be > 0');
  const perHead = Math.floor((total * 100) / people) / 100;
  const remainder = +(total - perHead * people).toFixed(2);
  const shares = Array(people).fill(perHead);
  shares[0] = +(shares[0] + remainder).toFixed(2);
  return shares;
}

// tests/money.spec.ts
import { describe, it, expect } from 'vitest';
import { splitBill } from '../src/money';

describe('splitBill', () => {
  it('divides evenly when possible', () => {
    expect(splitBill(100, 4)).toEqual([25, 25, 25, 25]);
  });

  it('puts remainder on the first payer', () => {
    expect(splitBill(10, 3)).toEqual([3.34, 3.33, 3.33]);
  });

  it('throws on invalid people', () => {
    expect(() => splitBill(100, 0)).toThrow();
  });

  it('handles zero total', () => {
    expect(splitBill(0, 5)).toEqual([0, 0, 0, 0, 0]);
  });
});
⚠️
Sinais de unit test ruim.(1) Mock gigante de 30 linhas — seu código tem acoplamento demais. (2) Testa implementação (“foi chamado N vezes”) em vez de resultado. (3) Quebra toda refatoração. Bom unit testa comportamento público, não detalhe interno.

Integração com Testcontainers

Testcontainers (disponível em Java, Node, Go, Python) sobe Postgres, Redis, Kafka real num container efêmero pro teste. Fim do “funciona no mock, quebra em prod”.

typescript
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { beforeAll, afterAll, describe, it, expect } from 'vitest';
import { Pool } from 'pg';
import { createOrder, findOrder } from '../src/orders';

describe('orders (integration)', () => {
  let container: Awaited<ReturnType<PostgreSqlContainer['start']>>;
  let pool: Pool;

  beforeAll(async () => {
    container = await new PostgreSqlContainer('postgres:15-alpine').start();
    pool = new Pool({ connectionString: container.getConnectionUri() });
    await pool.query(`CREATE TABLE orders (
      id TEXT PRIMARY KEY,
      user_id TEXT NOT NULL,
      amount NUMERIC(10,2) NOT NULL,
      created_at TIMESTAMPTZ DEFAULT now()
    )`);
  }, 60_000);

  afterAll(async () => {
    await pool.end();
    await container.stop();
  });

  it('persists and retrieves order', async () => {
    const order = await createOrder(pool, { userId: 'u_1', amount: 99.9 });
    const found = await findOrder(pool, order.id);
    expect(found).toMatchObject({ userId: 'u_1', amount: '99.90' });
  });
});

Property-Based Testing (a arma secreta)

typescript
import fc from 'fast-check';
import { splitBill } from '../src/money';

// propriedade 1: soma das partes bate o total
test('sum of shares equals total', () => {
  fc.assert(
    fc.property(
      fc.float({ min: 0, max: 1_000_000, noNaN: true }),
      fc.integer({ min: 1, max: 100 }),
      (total, people) => {
        const shares = splitBill(total, people);
        const sum = shares.reduce((a, b) => a + b, 0);
        expect(Math.abs(sum - total)).toBeLessThan(0.01);
      }
    )
  );
});

// propriedade 2: nenhuma parte é negativa
test('no negative share', () => {
  fc.assert(
    fc.property(
      fc.float({ min: 0, max: 1000, noNaN: true }),
      fc.integer({ min: 1, max: 50 }),
      (total, people) => splitBill(total, people).every(s => s >= 0)
    )
  );
});
Por que vira obsessão depois que experimenta. Property-based gera milhares de inputs, encontra o menor contraexemplo (shrinking) e reproduz deterministicamente. Em 30 minutos, pega bug de precisão, overflow, sinal, unicode, null implícito — coisas que você nunca pensaria em testar manualmente.

Contract Testing (Pact / OpenAPI)

Dois serviços (consumer e provider) têm contrato. Pact faz o consumer declarar a expectativa; provider roda contra essas expectativas em CI.

typescript
// CONSUMER (mobile-app) declara expectativa
import { PactV3, MatchersV3 as M } from '@pact-foundation/pact';

const provider = new PactV3({ consumer: 'mobile-app', provider: 'orders-api' });

it('GET /orders/:id returns 200 with body', async () => {
  provider
    .given('order o_1 exists')
    .uponReceiving('a request for order o_1')
    .withRequest({ method: 'GET', path: '/orders/o_1' })
    .willRespondWith({
      status: 200,
      headers: { 'content-type': 'application/json' },
      body: {
        id: 'o_1',
        amount: M.decimal(99.9),
        status: M.regex(/^(paid|pending|cancelled)$/, 'paid'),
      },
    });

  await provider.executeTest(async (mock) => {
    const res = await fetch(`${mock.url}/orders/o_1`);
    expect(res.status).toBe(200);
  });
});

// gera um arquivo pact.json que o PROVIDER vai rodar no CI dele
💡
Variações. (1) OpenAPI/Swagger contract test com ferramentas como Dredd ou Schemathesis valida que implementação bate com spec. (2) Schema compatibility em Avro/Protobuf é obrigatório em Kafka — quebra de contrato em event streaming é caos operacional.

Snapshot (com moderação)

Snapshot salva o output esperado. Útil para JSON, HTML, CLI output. Ruim quando vira lixo que ninguém lê.

typescript
test('invoice PDF structure', () => {
  const json = buildInvoiceJson({ user: 'u1', total: 100 });
  expect(json).toMatchSnapshot();
});
⚠️
Regras. (1) Snapshot sempre passa em code review — atualizar snapshot sem ler é bug entrando. (2) UsetoMatchInlineSnapshot() pra snapshots pequenos ficarem no próprio teste. (3) Nunca snapshot de estrutura com timestamps/UUIDs sem masking.

Mutation Testing (teste dos testes)

Mutation testing modifica seu código (troca > por >=, remove linha, inverte boolean) e verifica se algum teste quebra. Se não quebra = seus testes são cegos para aquele mutante.

bash
# JS/TS: stryker-mutator
npm install --save-dev @stryker-mutator/core @stryker-mutator/vitest-runner
npx stryker init
npx stryker run

# Output esperado:
# Mutation score: 87.5%  (pegamos 140/160 mutantes)
# Surviving mutants:
#   src/money.ts:12 - trocou `>` por `>=` — nenhum teste detectou
#   src/money.ts:18 - removeu linha de remainder — testes ainda passaram
Target de mutation score. 70%+ é bom, 85%+ é excelente, 100% é overkill. Prefira mutation alto em módulos críticos (pagamento, auth) e mutation razoável em CRUD.

Fuzz Testing

go
// Go 1.18+ tem fuzz nativo
// fuzz_test.go
func FuzzParseCPF(f *testing.F) {
    f.Add("12345678909")
    f.Add("111.111.111-11")
    f.Add("")

    f.Fuzz(func(t *testing.T, input string) {
        _, err := ParseCPF(input)
        // o invariante: não pode panicar mesmo em input lixo
        if err == nil && !IsValidCPF(input) {
            t.Errorf("parse aceitou CPF inválido: %q", input)
        }
    })
}

// Rodar: go test -fuzz=FuzzParseCPF -fuzztime=60s ./...
python
# Python com Hypothesis (property-based + fuzz-like)
from hypothesis import given, strategies as st
from mymodule import parse_cpf, is_valid

@given(st.text())
def test_parse_cpf_never_panics(s):
    try:
        result = parse_cpf(s)
        if result is not None:
            assert is_valid(result)
    except ValueError:
        pass  # expected for bad input
⚠️
Quando fuzz vira obrigatório. Código que parseia input não-confiável: JSON, protobuf, imagem, áudio, URL, CSV, regex customizado. Fuzz pega crash, infinite loop, consumo exponencial de memória — bugs que viram CVE se escaparem.

E2E: poucos, mas de verdade

typescript
// Playwright — golden path de checkout
import { test, expect } from '@playwright/test';

test('user completes checkout', async ({ page }) => {
  await page.goto('/products/camisa-ffv');
  await page.getByRole('button', { name: 'Adicionar ao carrinho' }).click();
  await page.getByRole('link', { name: 'Carrinho' }).click();
  await page.getByRole('button', { name: 'Finalizar' }).click();

  await page.getByLabel('E-mail').fill('test@ffv.com');
  await page.getByLabel('Cartão').fill('4242 4242 4242 4242');
  await page.getByLabel('CVV').fill('123');
  await page.getByRole('button', { name: 'Pagar' }).click();

  await expect(page.getByText('Pedido confirmado')).toBeVisible({ timeout: 10_000 });
});
⚠️
Regras pra não odiar E2E. (1) Só golden paths (top 5 fluxos). (2) Dados determinísticos (seeds, não produção). (3) Retry em flakiness + quarentena de teste instável. (4) Roda em stage, não em preview efêmero. (5) Falhou? Quem shipou conserta — não deixa apodrecer.

Chaos & Load

Load test (K6, Gatling)Simula N users concorrentes. Mede p50/p99, erro, throughput. Roda em ambiente semelhante a prod.
Soak testCarga baixa por muito tempo (6-24h). Pega memory leak, connection pool exhaustion, cron bugs.
Stress testSobe carga até quebrar. Identifica capacity ceiling.
Chaos (Chaos Mesh, LitmusChaos)Mata pod, corta rede, adiciona latência. Testa se o sistema aguenta falha parcial.
Onde rodarPreview env ou staging. NÃO no CI normal — demora demais. Pipeline separado, noturno.

Escolhendo testes para cada tipo de código

CódigoObrigatórioÚtilTalvez
Função pura (parse, format, calc)Unit + Property-basedMutationSnapshot
Handler/ControllerIntegração (DB real)Contract testE2E golden path
Gateway/Adapter de API externaContract test + UnitPactFuzz no parsing de resposta
UI (React/Vue)Component test (Testing Library)Visual regression (Chromatic)E2E Playwright
Worker/JobIntegração (queue real)Chaos (kill pod mid-job)Soak test
Migration SQLTeste "aplica + reverte" no CIDry-run em snapshot de prod-
Parser de input externoUnit + FuzzProperty-basedMutation

Flaky tests: o cancro silencioso

  • Causas comuns: timing (setTimeout, sleep), ordem de execução, estado compartilhado, timezone, Date.now(), aleatoriedade sem seed, rede não mockada, recurso não limpo.
  • Política sã: teste flaky entra em quarentena por 48h; dono conserta ou remove. Test com retry ilimitado é lixo que erode a confiança.
  • Detecte: CI que roda mesmo teste 3× em PR para detectar instabilidade antes do merge.

Dois cenários reais de decisão

📋 API de cálculo de frete com 15 regras regionais e faixas de peso

Unit + Property-based massivo + Snapshot de response

Lógica pura com muita combinação. Property-test verifica invariantes (frete nunca negativo, pesos maiores = fretes maiores-ou-iguais). Unit cobre regras nomeadas. Snapshot confere forma da resposta.

Alt: E2Eexagero aqui; testar via UI 500 combinações de frete é masoquismo.

📋 Microserviço de pagamento que integra com Stripe via webhook

Contract test + Integration + Fuzz

Webhook é input não-confiável (fuzz), contrato com Stripe pode mudar (contract test contra sandbox), persistência e side effects precisam de integração com DB real.

Alt: Só unitignoraria os bugs reais que aparecem em produção.

Perguntas típicas

TDD é obrigatório?

Não. TDD é uma técnica entre várias. Escreva o teste ANTES quando o design está claro, DEPOIS quando está explorando. O que importa é que o teste exista e teste comportamento, não a ordem.

Posso confiar em agent para escrever todos os testes?

Agent escreve rápido, mas tende a testar implementação e cobrir o óbvio. Humano revisa se testa invariante certa, edge case real, integração com DB/queue. Mutation score é o filtro de qualidade.

Qual cobertura mínima razoável?

Depende do módulo. Crítico (auth, pagamento): 85-95% linha + 80% mutation. Rota CRUD padrão: 70-80% linha. Log/aux scripts: não precisa meta.

Mock ou real em integração?

Real sempre que barato (Postgres em Testcontainers, Redis em Docker). Mock só onde não dá (Stripe, Salesforce, serviço interno caro de subir). Quanto mais mock, menos o teste representa prod.

Tests em produção?

Sim, chama-se synthetic monitoring: roda E2E reduzido contra produção a cada 5 minutos e alerta. Datadog Synthetics e Checkly fazem isso. Não substitui teste antes de deploy, mas pega bug que escapou.
Take-aways. (1) Cobertura é piso, mutation é teto. (2) Property-based pega bugs que humano não pensa. (3) Contract test é obrigatório em sistema distribuído. (4) Fuzz em parser é não-negociável. (5) E2E só em golden paths. (6) Flaky test é bug. (7) Próximo: segurança real, onde muita dessa estrutura vai ser testada seriamente.
🧩

Quiz rápido

4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito

Continue lendo