Kaique Mitsuo Silva Yamamoto
Arquitetura softwareFrontend webTypeScriptTestes

TDD — Test-Driven Development

O ciclo Red-Green-Refactor, quando usar TDD e quando não — exemplos práticos full-stack.

TDD — Test-Driven Development

TDD não é sobre testes. TDD é um método de design que usa testes como ferramenta. O teste vem antes do código — ele define o que o código deve fazer.

O ciclo: Red → Green → Refactor

1. RED     → Escreva um teste que FALHA (o código não existe ainda)
2. GREEN   → Escreva o código MÍNIMO para o teste passar
3. REFACTOR → Melhore o código sem quebrar o teste
4. Repita

Analogia

É como construir uma casa:

  • Red: "Preciso de uma porta que abre e fecha" (requisito/teste)
  • Green: Prega duas tábuas com uma dobradiça (solução mínima)
  • Refactor: Troca por uma porta de verdade, com fechadura (melhoria)
  • Se em algum momento a porta para de abrir → você sabe exatamente o que quebrou

Exemplo prático — Sistema de desconto (Backend)

Passo 1: RED — Escreve o teste primeiro

// services/__tests__/discount.service.test.ts
import { describe, it, expect } from 'vitest';

describe('DiscountService', () => {
  it('aplica 10% de desconto para compras acima de R$100', () => {
    const service = new DiscountService(); // ❌ não existe ainda
    const result = service.calculate(150);
    expect(result).toBe(135); // 150 * 0.9
  });

  it('não aplica desconto para compras abaixo de R$100', () => {
    const service = new DiscountService();
    const result = service.calculate(80);
    expect(result).toBe(80);
  });

  it('aplica 20% para compras acima de R$500', () => {
    const service = new DiscountService();
    const result = service.calculate(600);
    expect(result).toBe(480); // 600 * 0.8
  });
});
❌ RED — DiscountService não existe

Passo 2: GREEN — Código mínimo para passar

// services/discount.service.ts
export class DiscountService {
  calculate(amount: number): number {
    if (amount > 500) return amount * 0.8;
    if (amount > 100) return amount * 0.9;
    return amount;
  }
}
✅ GREEN — todos os testes passam

Passo 3: REFACTOR — Melhora sem quebrar

// services/discount.service.ts
interface DiscountTier {
  minAmount: number;
  discount: number; // 0.1 = 10%
}

const DISCOUNT_TIERS: DiscountTier[] = [
  { minAmount: 500, discount: 0.2 },
  { minAmount: 100, discount: 0.1 },
];

export class DiscountService {
  calculate(amount: number): number {
    const tier = DISCOUNT_TIERS.find(t => amount >= t.minAmount);
    return tier ? amount * (1 - tier.discount) : amount;
  }
}
✅ GREEN — testes ainda passam, código mais limpo

Passo 4: Novo teste → RED → GREEN → REFACTOR

it('aplica desconto cumulativo para cliente VIP', () => {
  const service = new DiscountService();
  const result = service.calculate(200, { isVip: true });
  expect(result).toBe(160); // 200 * 0.9 (base) * ~1.11... ou 20% flat para VIP
});
❌ RED — não aceita segundo parâmetro
→ GREEN → REFACTOR → repete

Exemplo prático — Componente React (Frontend)

RED — Teste primeiro

// components/__tests__/PriceTag.test.tsx
describe('PriceTag', () => {
  it('exibe preço formatado em BRL', () => {
    render(<PriceTag amount={29.9} />);
    expect(screen.getByText('R$\u00a029,90')).toBeInTheDocument();
  });

  it('mostra preço original riscado quando há desconto', () => {
    render(<PriceTag amount={29.9} originalPrice={49.9} />);
    expect(screen.getByText('R$\u00a049,90')).toBeInTheDocument();
    expect(screen.getByText('R$\u00a029,90')).toBeInTheDocument();
  });

  it('não mostra preço original quando não há desconto', () => {
    render(<PriceTag amount={29.9} />);
    expect(screen.queryByText(/riscado/i)).not.toBeInTheDocument();
  });
});

GREEN — Componente mínimo

// components/PriceTag.tsx
interface PriceTagProps {
  amount: number;
  originalPrice?: number;
}

export function PriceTag({ amount, originalPrice }: PriceTagProps) {
  return (
    <div>
      {originalPrice && <span className="line-through">{formatBRL(originalPrice)}</span>}
      <span>{formatBRL(amount)}</span>
    </div>
  );
}

REFACTOR — Melhora apresentação

export function PriceTag({ amount, originalPrice }: PriceTagProps) {
  const hasDiscount = originalPrice && originalPrice > amount;
  const discountPercent = hasDiscount
    ? Math.round((1 - amount / originalPrice) * 100)
    : 0;

  return (
    <div className="flex items-center gap-2">
      {hasDiscount && (
        <>
          <span className="text-gray-400 line-through">{formatBRL(originalPrice)}</span>
          <span className="text-green-600 text-sm">-{discountPercent}%</span>
        </>
      )}
      <span className="text-xl font-bold">{formatBRL(amount)}</span>
    </div>
  );
}

Quando usar TDD

Use TDD quandoNão use TDD quando
Regra de negócio claraExplorando/experimentando
Lógica complexa com muitos casosUI puramente visual
Funções purasPrototipagem rápida
Código que será reutilizadoScript descartável
Bug fix (reproduz o bug como teste)Configuração de infraestrutura

Regras de ouro do TDD

  1. Um teste por comportamento — não teste 5 coisas em um teste
  2. Nomes descritivosit('rejeita email sem @') não it('testa email')
  3. AAA: Arrange, Act, Assert — prepare, execute, verifique
  4. Nunca pule o refactor — código mínimo verde ≠ código bom
  5. Testes devem ser independentes — a ordem dos testes não importa

Referências

On this page