Testes Profissionais: pirâmide, propriedades, contrato e fuzz
- ⬜🛠️ Criando Agents Customizados: do subagent ao MCP(Engenharia de Software Moderna)
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)
Unit: rápido, puro, opinativo
// 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]);
});
});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”.
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)
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)
)
);
});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.
// 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 deleSnapshot (com moderação)
Snapshot salva o output esperado. Útil para JSON, HTML, CLI output. Ruim quando vira lixo que ninguém lê.
test('invoice PDF structure', () => {
const json = buildInvoiceJson({ user: 'u1', total: 100 });
expect(json).toMatchSnapshot();
});toMatchInlineSnapshot() 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.
# 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
Fuzz Testing
// 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 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 inputE2E: poucos, mas de verdade
// 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 });
});Chaos & Load
Escolhendo testes para cada tipo de código
| Código | Obrigatório | Útil | Talvez |
|---|---|---|---|
| Função pura (parse, format, calc) | Unit + Property-based | Mutation | Snapshot |
| Handler/Controller | Integração (DB real) | Contract test | E2E golden path |
| Gateway/Adapter de API externa | Contract test + Unit | Pact | Fuzz no parsing de resposta |
| UI (React/Vue) | Component test (Testing Library) | Visual regression (Chromatic) | E2E Playwright |
| Worker/Job | Integração (queue real) | Chaos (kill pod mid-job) | Soak test |
| Migration SQL | Teste "aplica + reverte" no CI | Dry-run em snapshot de prod | - |
| Parser de input externo | Unit + Fuzz | Property-based | Mutation |
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
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: E2E — exagero aqui; testar via UI 500 combinações de frete é masoquismo.
📋 Microserviço de pagamento que integra com Stripe via webhook
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ó unit — ignoraria os bugs reais que aparecem em produção.
Perguntas típicas
❓ TDD é obrigatório?
❓ Posso confiar em agent para escrever todos os testes?
❓ Qual cobertura mínima razoável?
❓ Mock ou real em integração?
❓ Tests em produção?
Quiz rápido
4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito