Testes

Pirâmide de Testes: Custo vs Confiança

A pirâmide de testes (Mike Cohn) define a proporção ideal entre tipos de teste. Cada nível tem um trade-off entre velocidade/custo e confiança/realismo:

         ╱╲
        ╱E2E╲         Poucos | Lentos | Caros | Alta confiança
       ╱──────╲        Testam o sistema inteiro (browser, API, DB)
      ╱Integra-╲
     ╱  ção     ╲     Moderados | Média velocidade | Média confiança
    ╱────────────╲     Testam boundaries reais (DB, APIs externas)
   ╱    Unit      ╲
  ╱────────────────╲   Muitos | Rápidos | Baratos | Baixa confiança
                       Testam funções isoladas, lógica pura

Trade-offs por nível:
  Unit:        ~1ms/teste, sem I/O, quebra com refactoring interno
  Integration: ~100ms/teste, I/O real, mais estável que unit
  E2E:         ~5s/teste, browser real, flaky mas alta confiança

Na prática, a proporção exata depende do tipo de aplicação:

Aplicação CRUD simples:     Aplicação com lógica complexa:
  E2E: 20%                    E2E: 5%
  Integration: 50%            Integration: 25%
  Unit: 30%                   Unit: 70%

Microserviços:              Frontend-heavy (SPA):
  E2E: 5%                    E2E: 30%
  Integration: 60%           Integration: 20%
  Contract: 20%              Unit: 50% (component tests)
  Unit: 15%

Unit Tests: Isolamento e Testabilidade

Um bom unit test testa uma unidade de comportamento (não necessariamente uma função). Deve ser rápido, determinístico e sem I/O:

// Função pura: fácil de testar, sem dependências externas
function calculateOrderTotal(
  items: Array<{ price: number; quantity: number }>,
  discountPercent: number,
  taxRate: number,
): number {
  if (discountPercent < 0 || discountPercent > 100) {
    throw new Error('Desconto deve estar entre 0 e 100');
  }

  const subtotal = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0,
  );
  const discounted = subtotal * (1 - discountPercent / 100);
  const total = discounted * (1 + taxRate);
  return Math.round(total * 100) / 100; // Arredonda para centavos
}

// Testes: cobrem happy path, edge cases e error cases
describe('calculateOrderTotal', () => {
  // Happy path
  it('deve calcular total com desconto e imposto', () => {
    const items = [
      { price: 100, quantity: 2 },  // 200
      { price: 50, quantity: 1 },   // 50
    ];
    // Subtotal: 250, desconto 10%: 225, imposto 8%: 243
    expect(calculateOrderTotal(items, 10, 0.08)).toBe(243);
  });

  // Edge cases
  it('deve retornar 0 para lista vazia', () => {
    expect(calculateOrderTotal([], 0, 0.08)).toBe(0);
  });

  it('deve lidar com desconto de 100%', () => {
    const items = [{ price: 100, quantity: 1 }];
    expect(calculateOrderTotal(items, 100, 0.08)).toBe(0);
  });

  it('deve arredondar corretamente para centavos', () => {
    const items = [{ price: 10.33, quantity: 3 }];
    // 30.99 * 1.08 = 33.4692 → 33.47
    expect(calculateOrderTotal(items, 0, 0.08)).toBe(33.47);
  });

  // Error cases
  it('deve lançar erro para desconto negativo', () => {
    expect(() => calculateOrderTotal([], -5, 0)).toThrow(
      'Desconto deve estar entre 0 e 100',
    );
  });

  it('deve lançar erro para desconto acima de 100', () => {
    expect(() => calculateOrderTotal([], 150, 0)).toThrow(
      'Desconto deve estar entre 0 e 100',
    );
  });
});

Dependency Injection para Testabilidade

Código acoplado a dependências externas é difícil de testar. Dependency Injection (DI) resolve isso invertendo o controle:

// ❌ Difícil de testar: dependência hardcoded
class OrderService {
  async createOrder(userId: string, items: CartItem[]) {
    const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
    const order = await db.query('INSERT INTO orders ...');
    await sendEmail(user.email, 'Pedido confirmado');  // Efeito colateral
    return order;
  }
}

// ✅ Testável: dependências injetadas
class OrderService {
  constructor(
    private readonly userRepo: UserRepository,
    private readonly orderRepo: OrderRepository,
    private readonly emailService: EmailService,
  ) {}

  async createOrder(userId: string, items: CartItem[]) {
    const user = await this.userRepo.findById(userId);
    if (!user) throw new UserNotFoundError(userId);

    const total = items.reduce((s, i) => s + i.price * i.qty, 0);
    const order = await this.orderRepo.create({ userId, items, total });

    await this.emailService.send(user.email, 'Pedido confirmado');
    return order;
  }
}

// No teste: injeta mocks/stubs em vez das dependências reais
describe('OrderService.createOrder', () => {
  it('deve criar pedido e enviar email', async () => {
    const userRepo = { findById: jest.fn().mockResolvedValue({ id: '1', email: 'a@b.com' }) };
    const orderRepo = { create: jest.fn().mockResolvedValue({ id: 'order_1' }) };
    const emailService = { send: jest.fn().mockResolvedValue(undefined) };

    const service = new OrderService(userRepo as any, orderRepo as any, emailService as any);
    const order = await service.createOrder('1', [{ price: 100, qty: 2 }]);

    expect(order.id).toBe('order_1');
    expect(orderRepo.create).toHaveBeenCalledWith(
      expect.objectContaining({ userId: '1', total: 200 }),
    );
    expect(emailService.send).toHaveBeenCalledWith('a@b.com', 'Pedido confirmado');
  });

  it('deve lançar erro se usuário não existe', async () => {
    const userRepo = { findById: jest.fn().mockResolvedValue(null) };
    const service = new OrderService(userRepo as any, {} as any, {} as any);

    await expect(service.createOrder('999', [])).rejects.toThrow(UserNotFoundError);
  });
});

Test Doubles: Mock vs Stub vs Spy vs Fake

Termos formais definidos por Gerard Meszaros (xUnit Test Patterns):

Tipo       O que faz                           Quando usar
────────   ──────────────────────────────────  ─────────────────────────────
Dummy      Preenche parâmetro, nunca é usado   Satisfazer assinatura de função
Stub       Retorna respostas pré-definidas     Controlar dados de entrada indireta
Spy        Registra chamadas para verificação  Verificar que algo foi chamado
Mock       Stub + verificação de interação     Verificar COMO dependência foi usada
Fake       Implementação simplificada real     Substituir infra pesada (in-memory DB)
// STUB: controla o que a dependência retorna
const paymentGateway = {
  charge: jest.fn().mockResolvedValue({ success: true, transactionId: 'tx_123' }),
};

// SPY: verifica que a dependência foi chamada corretamente
const logger = { info: jest.fn(), error: jest.fn() };
// ... executa código ...
expect(logger.info).toHaveBeenCalledWith('Pagamento processado', { txId: 'tx_123' });
expect(logger.error).not.toHaveBeenCalled();

// MOCK: stub + expectativa de interação (ordem, número de chamadas)
const emailService = { send: jest.fn() };
// ... executa código ...
expect(emailService.send).toHaveBeenCalledTimes(1);
expect(emailService.send).toHaveBeenCalledWith(
  expect.objectContaining({ to: 'user@example.com', subject: expect.stringContaining('Confirmação') }),
);

// FAKE: implementação funcional simplificada
class FakeUserRepository implements UserRepository {
  private users = new Map<string, User>();

  async findById(id: string): Promise<User | null> {
    return this.users.get(id) || null;
  }

  async save(user: User): Promise<void> {
    this.users.set(user.id, { ...user });
  }

  // Helper para testes
  seed(users: User[]): void {
    users.forEach(u => this.users.set(u.id, u));
  }
}

Integration Tests: Boundaries Reais

Integration tests verificam que componentes funcionam juntos. Teste com o banco real (não mocks):

// Testcontainers: sobe container Docker para cada suíte de testes
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';

describe('UserRepository (integration)', () => {
  let container: StartedPostgreSqlContainer;
  let pool: Pool;
  let repo: UserRepository;

  beforeAll(async () => {
    // Sobe PostgreSQL real em container
    container = await new PostgreSqlContainer('postgres:16')
      .withDatabase('testdb')
      .start();

    pool = new Pool({ connectionString: container.getConnectionUri() });

    // Roda migrações
    await runMigrations(pool);
    repo = new UserRepository(pool);
  }, 30000); // Timeout maior para start do container

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

  beforeEach(async () => {
    // Limpa dados entre testes (TRUNCATE é mais rápido que DELETE)
    await pool.query('TRUNCATE users CASCADE');
  });

  it('deve criar e buscar usuário', async () => {
    const created = await repo.create({
      name: 'Lucas',
      email: 'lucas@example.com',
    });

    expect(created.id).toBeDefined();

    const found = await repo.findById(created.id);
    expect(found).toEqual(expect.objectContaining({
      name: 'Lucas',
      email: 'lucas@example.com',
    }));
  });

  it('deve lançar erro para email duplicado', async () => {
    await repo.create({ name: 'A', email: 'dup@test.com' });

    await expect(
      repo.create({ name: 'B', email: 'dup@test.com' }),
    ).rejects.toThrow(/unique.*email/i);
  });

  it('deve aplicar soft delete corretamente', async () => {
    const user = await repo.create({ name: 'X', email: 'x@test.com' });
    await repo.softDelete(user.id);

    const found = await repo.findById(user.id);
    expect(found).toBeNull(); // findById ignora soft-deleted

    const foundIncludingDeleted = await repo.findById(user.id, {
      includeDeleted: true,
    });
    expect(foundIncludingDeleted?.deletedAt).toBeDefined();
  });
});

API Integration Test

import request from 'supertest';
import { app } from '../src/app';

describe('POST /api/users (integration)', () => {
  beforeEach(async () => {
    await db.query('TRUNCATE users CASCADE');
  });

  it('deve criar usuário e retornar 201', async () => {
    // Arrange: nada (banco limpo pelo beforeEach)

    // Act
    const res = await request(app)
      .post('/api/users')
      .send({ email: 'test@test.com', name: 'Test User' })
      .expect('Content-Type', /json/);

    // Assert: resposta HTTP
    expect(res.status).toBe(201);
    expect(res.body).toMatchObject({
      id: expect.any(String),
      email: 'test@test.com',
      name: 'Test User',
    });

    // Assert: persistência real no banco
    const { rows } = await db.query(
      'SELECT * FROM users WHERE email = $1',
      ['test@test.com'],
    );
    expect(rows).toHaveLength(1);
    expect(rows[0].name).toBe('Test User');
  });

  it('deve retornar 409 para email duplicado', async () => {
    await request(app)
      .post('/api/users')
      .send({ email: 'dup@test.com', name: 'First' });

    const res = await request(app)
      .post('/api/users')
      .send({ email: 'dup@test.com', name: 'Second' });

    expect(res.status).toBe(409);
    expect(res.body.error).toMatch(/already exists/i);
  });

  it('deve retornar 422 para payload inválido', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ email: 'not-an-email', name: '' });

    expect(res.status).toBe(422);
    expect(res.body.errors).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ field: 'email' }),
        expect.objectContaining({ field: 'name' }),
      ]),
    );
  });
});

E2E Tests: Cypress e Playwright

E2E tests verificam fluxos completos do ponto de vista do usuário:

// Playwright: mais rápido e estável que Cypress para E2E
import { test, expect } from '@playwright/test';

test.describe('Fluxo de checkout', () => {
  test.beforeEach(async ({ page }) => {
    // Seed de dados via API (não via UI — mais rápido e determinístico)
    await fetch('http://localhost:3000/api/test/seed', { method: 'POST' });
    await page.goto('http://localhost:3000/login');
    await page.fill('[data-testid="email"]', 'user@test.com');
    await page.fill('[data-testid="password"]', 'password123');
    await page.click('[data-testid="login-button"]');
    await page.waitForURL('/dashboard');
  });

  test('deve completar compra com cartão', async ({ page }) => {
    // Navegar para produto
    await page.goto('/products/teclado-mecanico');
    await page.click('[data-testid="add-to-cart"]');

    // Ir para checkout
    await page.click('[data-testid="cart-icon"]');
    await page.click('[data-testid="checkout-button"]');

    // Preencher pagamento
    await page.fill('[data-testid="card-number"]', '4242424242424242');
    await page.fill('[data-testid="card-expiry"]', '12/28');
    await page.fill('[data-testid="card-cvc"]', '123');

    // Confirmar
    await page.click('[data-testid="confirm-purchase"]');

    // Verificar sucesso
    await expect(page.locator('[data-testid="success-message"]'))
      .toHaveText(/Pedido confirmado/);
    await expect(page.locator('[data-testid="order-number"]'))
      .toBeVisible();
  });
});

// Trade-offs de E2E:
// ✅ Alta confiança: testa o sistema real
// ❌ Lento: ~5-30s por teste
// ❌ Flaky: depende de timing, rede, rendering
// ❌ Caro de manter: qualquer mudança de UI pode quebrar

TDD: Red-Green-Refactor

Test-Driven Development segue um ciclo de três passos:

RED:      Escreva um teste que falha (define o comportamento desejado)
GREEN:    Escreva o código MÍNIMO para o teste passar
REFACTOR: Melhore o código mantendo os testes verdes

Ciclo:  RED → GREEN → REFACTOR → RED → GREEN → REFACTOR → ...
// Exemplo: implementar validação de CPF com TDD

// RED: teste primeiro (função ainda não existe)
describe('validateCPF', () => {
  it('deve aceitar CPF válido', () => {
    expect(validateCPF('529.982.247-25')).toBe(true);
  });

  it('deve rejeitar CPF com dígitos repetidos', () => {
    expect(validateCPF('111.111.111-11')).toBe(false);
  });

  it('deve rejeitar CPF com dígito verificador errado', () => {
    expect(validateCPF('529.982.247-26')).toBe(false);
  });

  it('deve aceitar CPF sem formatação', () => {
    expect(validateCPF('52998224725')).toBe(true);
  });

  it('deve rejeitar string vazia', () => {
    expect(validateCPF('')).toBe(false);
  });
});

// GREEN: implementação mínima (pode ser feia, o importante é passar)
function validateCPF(cpf: string): boolean {
  const cleaned = cpf.replace(/\D/g, '');
  if (cleaned.length !== 11) return false;
  if (/^(\d)\1{10}$/.test(cleaned)) return false;

  const calcDigit = (slice: string, factor: number): number => {
    let sum = 0;
    for (let i = 0; i < slice.length; i++) {
      sum += parseInt(slice[i]) * (factor - i);
    }
    const remainder = sum % 11;
    return remainder < 2 ? 0 : 11 - remainder;
  };

  const d1 = calcDigit(cleaned.slice(0, 9), 10);
  const d2 = calcDigit(cleaned.slice(0, 10), 11);
  return d1 === parseInt(cleaned[9]) && d2 === parseInt(cleaned[10]);
}

// REFACTOR: melhorar sem mudar comportamento (extrair funções, renomear, etc.)

Quando TDD funciona bem vs quando não

TDD funciona bem para:                  TDD não funciona bem para:
────────────────────────                ──────────────────────────
• Lógica de negócio pura               • UI/UX exploratória
• Validações e regras complexas         • Protótipos rápidos
• Algoritmos com edge cases claros      • Código que interage com APIs externas
• Refactoring de código legado          • Quando os requisitos são vagos
• Libraries e utils                     • Performance tuning

Testing Patterns

AAA (Arrange-Act-Assert)

it('deve aplicar cupom de desconto ao pedido', () => {
  // Arrange: preparar o cenário
  const order = new Order([
    { product: 'Teclado', price: 300 },
    { product: 'Mouse', price: 150 },
  ]);
  const coupon = new Coupon('SAVE20', 20); // 20% off

  // Act: executar a ação sendo testada
  order.applyCoupon(coupon);

  // Assert: verificar o resultado
  expect(order.total).toBe(360);          // 450 * 0.8
  expect(order.discount).toBe(90);
  expect(order.couponCode).toBe('SAVE20');
});

Given-When-Then (BDD style)

describe('Sistema de fidelidade', () => {
  describe('dado um cliente com 500 pontos', () => {
    const customer = createCustomer({ points: 500 });

    describe('quando resgata um prêmio de 300 pontos', () => {
      const result = customer.redeemReward({ cost: 300 });

      it('então deve debitar os pontos', () => {
        expect(customer.points).toBe(200);
      });

      it('então deve retornar sucesso', () => {
        expect(result.success).toBe(true);
      });
    });

    describe('quando tenta resgatar prêmio de 600 pontos', () => {
      it('então deve lançar erro de saldo insuficiente', () => {
        expect(() => customer.redeemReward({ cost: 600 }))
          .toThrow('Pontos insuficientes');
      });
    });
  });
});

Parametrized Tests

// Jest: test.each para testes parametrizados
describe('converterMoeda', () => {
  test.each([
    ['USD', 'BRL', 100, 500],     // $100 = R$500
    ['EUR', 'BRL', 100, 550],     // €100 = R$550
    ['BRL', 'USD', 500, 100],     // R$500 = $100
    ['USD', 'USD', 100, 100],     // Mesma moeda = mesmo valor
    ['USD', 'BRL', 0, 0],         // Zero = zero
  ])('deve converter %s → %s: %d → %d', (from, to, amount, expected) => {
    expect(converterMoeda(from, to, amount)).toBe(expected);
  });

  // Parametrized para error cases
  test.each([
    ['XXX', 'BRL', 'Moeda não suportada: XXX'],
    ['USD', 'YYY', 'Moeda não suportada: YYY'],
  ])('deve lançar erro para moeda inválida: %s → %s', (from, to, errorMsg) => {
    expect(() => converterMoeda(from, to, 100)).toThrow(errorMsg);
  });
});

Code Coverage: Por que 100% Não Basta

Coverage mede quais linhas foram executadas, não se o comportamento está correto:

Tipo de Coverage        O que mede                    Exemplo não coberto
──────────────────     ──────────────────────────     ────────────────────
Statement coverage     Linhas executadas              Linha nunca alcançada
Branch coverage        Caminhos if/else               else nunca testado
Function coverage      Funções chamadas               Função auxiliar ignorada
Line coverage          Linhas (≈ statement)           Similar ao statement

Por que 100% coverage é enganoso:
// Essa função tem 100% de line coverage com UM teste
function divide(a: number, b: number): number {
  return a / b;
}

test('divide 10 by 2', () => {
  expect(divide(10, 2)).toBe(5);
});
// 100% line coverage! Mas:
//   divide(10, 0) → Infinity (bug!)
//   divide(0, 0) → NaN (bug!)
//   divide(-10, 3) → -3.333... (comportamento esperado?)

// Coverage razoável como GUIDELINE (não como meta):
//   70-80% é pragmático para a maioria dos projetos
//   Foque em branch coverage (mais útil que line coverage)
//   Coverage alto em lógica de negócio, baixo em boilerplate

Mutation Testing: Stryker

Mutation testing responde: “Se eu introduzir um bug no código, algum teste falha?”

Como funciona:
  1. Stryker cria "mutantes" — cópias do código com pequenas modificações
  2. Cada mutante é testado contra sua suíte de testes
  3. Se um teste falha → mutante "morto" (BOM: seus testes detectaram a mudança)
  4. Se nenhum teste falha → mutante "sobreviveu" (RUIM: bug passou despercebido)

Mutation Score = mutantes mortos / total de mutantes × 100%

Tipos de mutações:
  • Arithmetic: a + b → a - b
  • Conditional: if (a > b) → if (a >= b)
  • Boolean: true → false
  • String: "hello" → ""
  • Remove statement: delete uma linha inteira
  • Return value: return x → return 0
// stryker.conf.js
module.exports = {
  mutator: 'typescript',
  packageManager: 'npm',
  reporters: ['html', 'clear-text', 'progress'],
  testRunner: 'jest',
  coverageAnalysis: 'perTest',
  thresholds: {
    high: 80,
    low: 60,
    break: 50,   // Falha o CI se mutation score < 50%
  },
  mutate: [
    'src/**/*.ts',
    '!src/**/*.test.ts',
    '!src/**/*.spec.ts',
  ],
};

// Exemplo de mutante que sobrevive (teste fraco):
// Código original:
function isEligibleForDiscount(age: number, purchases: number): boolean {
  return age >= 18 && purchases > 5;
}

// Mutante: age >= 18 → age > 18
// Se nenhum teste usa age = 18, o mutante sobrevive!
// Solução: adicionar teste com boundary value:
test('deve aceitar idade exatamente 18', () => {
  expect(isEligibleForDiscount(18, 10)).toBe(true);
});

Contract Testing: Pact

Em microserviços, contract testing garante que consumer e provider concordam sobre a API, sem precisar rodar ambos juntos:

// Consumer side: define as expectativas (contrato)
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike, string, integer } = MatchersV3;

const provider = new PactV3({
  consumer: 'OrderService',
  provider: 'UserService',
});

describe('UserService contract', () => {
  it('deve retornar usuário por ID', async () => {
    // Define a interação esperada
    await provider
      .given('usuário com ID 123 existe')
      .uponReceiving('request para buscar usuário 123')
      .withRequest({
        method: 'GET',
        path: '/api/users/123',
        headers: { Authorization: string('Bearer token') },
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          id: like('123'),
          name: like('Lucas'),
          email: like('lucas@example.com'),
          createdAt: like('2025-01-01T00:00:00Z'),
        },
      });

    // Executa o teste contra o mock do Pact
    await provider.executeTest(async (mockServer) => {
      const client = new UserServiceClient(mockServer.url);
      const user = await client.getUser('123');

      expect(user.id).toBe('123');
      expect(user.name).toBeDefined();
      expect(user.email).toContain('@');
    });
  });
});

// Provider side: verifica que a implementação real satisfaz os contratos
// Pact Broker armazena os contratos e permite verificação no CI do provider
Fluxo de Contract Testing:
─────────────────────────
  1. Consumer gera contrato (Pact file) → publica no Pact Broker
  2. Provider CI busca contratos → roda verificação contra implementação real
  3. Se provider quebra contrato → CI falha ANTES do deploy
  4. can-i-deploy: verifica se consumer e provider são compatíveis antes do deploy

Vantagens sobre E2E:
  • Rápido (não precisa subir todos os serviços)
  • Determinístico (sem dependência de ambiente)
  • Identifica exatamente qual contrato quebrou
  • Independente de deploy order

Load Testing: k6 e Artillery

Load testing valida se o sistema aguenta a carga esperada:

// k6: script de load test
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 50 },   // Ramp-up: 0 → 50 VUs em 30s
    { duration: '2m', target: 50 },     // Sustain: 50 VUs por 2 minutos
    { duration: '30s', target: 200 },   // Spike: sobe para 200 VUs
    { duration: '1m', target: 200 },    // Sustain spike
    { duration: '30s', target: 0 },     // Ramp-down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1000'],  // p95 < 500ms, p99 < 1s
    http_req_failed: ['rate<0.01'],                    // < 1% de erros
    http_reqs: ['rate>100'],                           // > 100 req/s throughput
  },
};

export default function () {
  const res = http.get('https://api.example.com/products');

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
    'body has products': (r) => JSON.parse(r.body).length > 0,
  });

  sleep(1); // Simula think time entre requests
}

// Conceitos de load testing:
// Throughput: requests por segundo que o sistema processa
// Latency percentiles:
//   p50 (mediana): metade dos requests são mais rápidos
//   p95: 95% dos requests são mais rápidos (SLA típico)
//   p99: quase todos — mas os 1% piores podem ser muito lentos
// Saturation point: throughput para de crescer mesmo com mais carga
// Breaking point: sistema começa a retornar erros (5xx, timeouts)

Flaky Tests: Causas e Soluções

Testes flaky (que passam e falham intermitentemente) destroem a confiança na suíte:

Causa                     Exemplo                          Solução
────────────────────     ────────────────────────────     ──────────────────────────
Timing / Race condition  setTimeout, Promise.race          Usar waitFor, retries, polling
Shared state             Teste A modifica global que       Isolamento: beforeEach cleanup,
                         Teste B precisa limpo             containers/DBs separados
Order dependency         Teste B depende de dado           Cada teste cria seus próprios
                         criado pelo Teste A               dados (setup independente)
External dependency      API externa timeout               Mock APIs externas em testes
Date/Time dependency     new Date() retorna valor          jest.useFakeTimers(),
                         diferente                         clock.tick()
Random data              Math.random() gera flakiness      Seed fixo para geradores
Async cleanup            Teste termina antes do cleanup    await de todas as promises
Port conflicts           Dois testes usam mesma porta      Portas dinâmicas (port 0)
// ❌ Flaky: depende de timing
test('deve expirar sessão após 5 minutos', async () => {
  const session = createSession();
  await new Promise(r => setTimeout(r, 300000)); // Esperar 5 minutos?!
  expect(session.isExpired()).toBe(true);
});

// ✅ Estável: usa fake timers
test('deve expirar sessão após 5 minutos', () => {
  jest.useFakeTimers();
  const session = createSession();

  jest.advanceTimersByTime(4 * 60 * 1000); // 4 minutos
  expect(session.isExpired()).toBe(false);

  jest.advanceTimersByTime(1 * 60 * 1000); // +1 minuto = 5 total
  expect(session.isExpired()).toBe(true);

  jest.useRealTimers();
});

// ❌ Flaky: shared state entre testes
let counter = 0;
test('incrementa', () => { counter++; expect(counter).toBe(1); });
test('incrementa de novo', () => { counter++; expect(counter).toBe(2); }); // Depende da ordem!

// ✅ Estável: cada teste é independente
test('incrementa', () => { let counter = 0; counter++; expect(counter).toBe(1); });
test('incrementa de novo', () => { let counter = 0; counter++; expect(counter).toBe(1); });

// Estratégia para lidar com flaky tests no CI:
// 1. Quarentena: mova para pasta @flaky, rode separado, não bloqueie merge
// 2. Retry: jest-circus retry (maxRetries: 2), mas isso MASCARA o problema
// 3. Fix root cause: identifique a causa real (geralmente shared state ou timing)
// 4. Randomize order: jest --randomize para detectar order dependency

Testes são infraestrutura de confiança. Uma suíte que é ignorada porque “sempre tem um teste falhando” é pior que não ter testes — ela dá falsa segurança enquanto esconde bugs reais.


Referencias e Fontes

  • “xUnit Test Patterns” — Gerard Meszaros — Catalogo completo de patterns e anti-patterns para testes automatizados
  • Jest Documentationhttps://jestjs.io/docs/getting-started — Documentacao oficial do Jest, framework de testes para JavaScript e TypeScript
  • Pact Documentationhttps://docs.pact.io/ — Documentacao oficial do Pact, ferramenta de contract testing para integracao entre servicos