# Test-Driven Development (TDD): Guia Prático para Implementação

**Meta Description:** Aprenda TDD do zero: ciclo Red-Green-Refactor, baby steps, mocks e práticas essenciais para escrever código testável e robusto.

---

## O Que é TDD?

Test-Driven Development (TDD) é uma metodologia de desenvolvimento onde você escreve os testes antes do código de produção. O ciclo é simples: Red (falha), Green (passa), Refactor (melhora).

### Ciclo Red-Green-Refactor

```
    RED              GREEN            REFACTOR
     ↓                ↓                 ↓
┌─────────┐     ┌─────────┐      ┌─────────────┐
│Escreva  │     │Faça o   │      │Melhore o    │
│o teste  │ →   │código   │ →    │código sem   │
│que      │     │mínimo   │      │mudar o      │
│falha    │     │passar   │      │comportamento│
└─────────┘     └─────────┘      └─────────────┘
     ↑                                   │
     └───────────────────────────────────┘
                 (Repetir)
```

---

## Por Que TDD?

### Benefícios Quantificados

| Métrica | Com TDD | Sem TDD |
|---------|---------|---------|
| Defeitos em produção | 40-60% menos | Baseline |
| Cobertura de testes | 90-100% | 20-50% |
| Confiança em refactoring | Alta | Baixa |
| Tempo de debugging | 30-50% menos | Baseline |
| Custo de manutenção | 40% menor | Baseline |

### Quando Usar TDD

**TDD BRILHA quando:**
- Algoritmos complexos
- APIs e bibliotecas
- Refactoring de código legado
- Código compartilhado
- Requisitos claros

**TDD é mais difícil quando:**
- UI layouts complexos
- Exploratory features
- Requisitos muito instáveis
- Prototipagem rápida

---

## TDD em Diferentes Linguagens

### Python com pytest

```python
# 1. RED - Escrever teste que falha
import pytest

def test_somar_dois_numeros_positivos():
    # Dado
    a = 2
    b = 3
    
    # Quando
    resultado = somar(a, b)
    
    # Então
    assert resultado == 5

# 2. GREEN - Implementar código mínimo
def somar(a, b):
    return 5  # Hardcoded para fazer passar

# 3. REFACTOR - Implementar corretamente
def somar(a, b):
    return a + b
```

### Exemplo Completo: Calculadora de Desconto

#### RED - Testes Iniciais

```python
# tests/test_desconto.py
import pytest
from desconto import CalculadoraDesconto

class TestCalculadoraDesconto:
    def setup_method(self):
        self.calc = CalculadoraDesconto()
    
    def test_desconto_de_10_porcento(self):
        """Cliente com 10 compras recebe 10% de desconto"""
        resultado = self.calc.calcular(valor=100, quantidade_compras=10)
        assert resultado == 90
    
    def test_desconto_de_20_porcento(self):
        """Cliente com 20 compras recebe 20% de desconto"""
        resultado = self.calc.calcular(valor=100, quantidade_compras=20)
        assert resultado == 80
    
    def test_sem_desconto(self):
        """Cliente novo (0 compras) não recebe desconto"""
        resultado = self.calc.calcular(valor=100, quantidade_compras=0)
        assert resultado == 100
    
    def test_desconto_maximo_de_30_porcento(self):
        """Cliente VIP (50+ compras) recebe no máximo 30%"""
        resultado = self.calc.calcular(valor=100, quantidade_compras=100)
        assert resultado == 70
    
    def test_valor_negativo_deve_falhar(self):
        """Valor negativo deve lançar exceção"""
        with pytest.raises(ValueError):
            self.calc.calcular(valor=-100, quantidade_compras=10)
    
    @pytest.mark.parametrize("valor,compras,esperado", [
        (100, 5, 100),   # Sem desconto
        (100, 10, 90),   # 10%
        (100, 20, 80),   # 20%
        (100, 50, 70),   # 30%
        (200, 10, 180),  # 10% em valor maior
    ])
    def test_parametrizado(self, valor, compras, esperado):
        resultado = self.calc.calcular(valor=valor, quantidade_compras=compras)
        assert resultado == esperado
```

#### GREEN - Implementação Mínima

```python
# desconto.py
class CalculadoraDesconto:
    def calcular(self, valor, quantidade_compras):
        if valor < 0:
            raise ValueError("Valor não pode ser negativo")
        
        if quantidade_compras >= 50:
            desconto = 0.30
        elif quantidade_compras >= 20:
            desconto = 0.20
        elif quantidade_compras >= 10:
            desconto = 0.10
        else:
            desconto = 0
        
        return valor * (1 - desconto)
```

#### REFACTOR - Melhorias

```python
# desconto.py (refatorado)
from dataclasses import dataclass

@dataclass
class RegraDesconto:
    min_compras: int
    max_compras: int | None
    percentual: float

REGRAS = [
    RegraDesconto(min_compras=50, max_compras=None, percentual=30),
    RegraDesconto(min_compras=20, max_compras=49, percentual=20),
    RegraDesconto(min_compras=10, max_compras=19, percentual=10),
]

class CalculadoraDesconto:
    def calcular(self, valor: float, quantidade_compras: int) -> float:
        if valor < 0:
            raise ValueError("Valor não pode ser negativo")
        
        for regra in REGRAS:
            if quantidade_compras >= regra.min_compras:
                if regra.max_compras is None or quantidade_compras <= regra.max_compras:
                    return valor * (1 - regra.percentual / 100)
        
        return valor
```

### JavaScript com Jest

```javascript
// 1. RED - Testes
describe('CalculadoraFrete', () => {
  const calculadora = new CalculadoraFrete();
  
  test('frete para SP capital', () => {
    expect(calculadora.calcular('SP', 'São Paulo')).toBe(15.90);
  });
  
  test('frete para RJ', () => {
    expect(calculadora.calcular('RJ', 'Rio de Janeiro')).toBe(20.00);
  });
  
  test('frete para outros estados', () => {
    expect(calculadora.calcular('MG', 'Belo Horizonte')).toBe(30.00);
  });
  
  test('frete grátis para SP com compra > 200', () => {
    expect(calculadora.calcular('SP', 'São Paulo', 250)).toBe(0);
  });
});

// 2. GREEN - Implementação
class CalculadoraFrete {
  calcular(estado, cidade, valorCompra = 0) {
    if (estado === 'SP' && valorCompra > 200) return 0;
    if (estado === 'SP') return 15.90;
    if (estado === 'RJ') return 20.00;
    return 30.00;
  }
}
```

### Java com JUnit 5

```java
// Teste
class CarrinhoTest {
    
    @Test
    void deveCalcularTotalComDesconto() {
        // Given
        Carrinho carrinho = new Carrinho();
        carrinho.adicionar(new Produto("Camiseta", 50.00, 2));
        carrinho.adicionar(new Produto("Calça", 100.00, 1));
        
        // When
        double total = carrinho.calcularTotal();
        
        // Then
        assertEquals(200.00, total, 0.01);
    }
    
    @Test
    void deveAplicarCupomDesconto() {
        // Given
        Carrinho carrinho = new Carrinho();
        carrinho.adicionar(new Produto("Camiseta", 100.00, 1));
        
        // When
        carrinho.aplicarCupom(new Cupom("DESCONTO10", 10));
        double total = carrinho.calcularTotal();
        
        // Then
        assertEquals(90.00, total, 0.01);
    }
    
    @ParameterizedTest
    @CsvSource({
        "100, 10, 90",
        "200, 10, 180",
        "100, 20, 80"
    })
    void deveCalcularComDescontoParametrizado(
        double valor, double desconto, double esperado
    ) {
        Carrinho carrinho = new Carrinho();
        carrinho.adicionar(new Produto("Item", valor, 1));
        carrinho.aplicarCupom(new Cupom("TESTE", desconto));
        
        assertEquals(esperado, carrinho.calcularTotal(), 0.01);
    }
}
```

---

## Baby Steps: Implementação Incremental

### Exemplo: Fibonacci

```python
# Passo 1: O menor teste possível
def test_fibonacci_0():
    assert fibonacci(0) == 0

# Implementação mínima
def fibonacci(n):
    return 0

# Passo 2: Segundo valor
def test_fibonacci_1():
    assert fibonacci(1) == 1

def fibonacci(n):
    if n == 0:
        return 0
    return 1  # Ainda não está correto!

# Passo 3: Terceiro valor
def test_fibonacci_2():
    assert fibonacci(2) == 1

# GREEN: Implementação correta (forma recursiva)
def fibonacci(n):
    if n <= 0:
        return 0
    if n == 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

# GREEN: Implementação melhor (forma iterativa)
def fibonacci(n):
    if n <= 0:
        return 0
    if n == 1:
        return 1
    
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b
```

---

## Mocks e Stubs

### Por Que Usar Mocks?

Mocks são essenciais para:
- Isolar unidade sob teste
- Simular dependências externas
- Controlar cenários de teste
- Aumentar velocidade dos testes

### Exemplo: Service com Dependência

```python
# 1. Teste com mock
from unittest.mock import Mock, patch

def test_pedido_service_cria_pedido_com_sucesso():
    # Given
    email_service = Mock()
    email_service.enviar = Mock(return_value=True)
    
    payment_gateway = Mock()
    payment_gateway.cobrar = Mock(return_value={
        "status": "approved",
        "transaction_id": "tx_123"
    })
    
    repository = Mock()
    repository.salvar = Mock(return_value=1)
    
    service = PedidoService(
        email_service=email_service,
        payment_gateway=payment_gateway,
        repository=repository
    )
    
    # When
    resultado = service.criar_pedido(
        cliente_id=1,
        itens=[{"produto_id": 1, "quantidade": 2}],
        forma_pagamento="credit_card"
    )
    
    # Then
    assert resultado["status"] == "confirmed"
    assert resultado["pedido_id"] == 1
    
    # Verificar chamadas
    payment_gateway.cobrar.assert_called_once()
    repository.salvar.assert_called_once()
    email_service.enviar.assert_called_once()
```

### Spy vs Mock vs Stub

| Tipo | Comportamento | Uso |
|------|---------------|-----|
| **Stub** | Respostas pré-programadas | Fornecer dados |
| **Mock** | Comportamento verificado | Verificar interações |
| **Spy** | Wrapper do real + verificação | Comportamento real + verificação |

```python
# Stub
repository = Mock()
repository.buscar_por_id = Mock(return_value={"id": 1, "nome": "Produto"})

# Spy (usando wraps)
with patch('app.repository') as mock_repo:
    mock_repo.buscar_por_id.return_value = {"id": 1}
    
    service = MyService(mock_repo)
    service.algum_metodo()
    
    # Verificar que foi chamado
    mock_repo.buscar_por_id.assert_called_once_with(1)
```

---

## TDD em Prática: Kata de String Calculator

### Requisitos

1. Criar calculadora que recebe string vazia retorna 0
2. Recebe "1" retorna 1
3. Recebe "1,2" retorna 3
4. Recebe "1\n2,3" retorna 6
5. Suporta delimitador customizado "//;\n1;2" retorna 3

### Solução TDD

```python
# Step 1: String vazia
def test_string_vazia_retorna_zero():
    assert string_calculator("") == 0

def string_calculator(numbers):
    return 0

# Step 2: Um número
def test_um_numero():
    assert string_calculator("1") == 1

def string_calculator(numbers):
    if not numbers:
        return 0
    return int(numbers)

# Step 3: Dois números separados por vírgula
def test_dois_numeros():
    assert string_calculator("1,2") == 3

def string_calculator(numbers):
    if not numbers:
        return 0
    return sum(int(n) for n in numbers.split(','))

# Step 4: Múltiplos números
def test_multiplos_numeros():
    assert string_calculator("1,2,3,4,5") == 15

def string_calculator(numbers):
    if not numbers:
        return 0
    delimiters = [',', '\n']
    parts = numbers.replace('\n', ',')
    return sum(int(n) for n in parts.split(','))

# Step 5: Delimitador customizado
def test_delimitador_customizado():
    assert string_calculator("//;\n1;2;3") == 6

def string_calculator(numbers):
    if not numbers:
        return 0
    
    # Detectar delimitador customizado
    if numbers.startswith('//'):
        end_header = numbers.index('\n')
        delimiter = numbers[2:end_header]
        numbers = numbers[end_header + 1:]
        numbers = numbers.replace(delimiter, ',')
    
    return sum(int(n) for n in numbers.replace('\n', ',').split(','))
```

---

## Conclusão

TDD é mais que escrever testes primeiro - é uma filosofia de design que resulta em código mais limpo, testável e manutenível. As chaves para dominar TDD são:

1. **Prática constante** - Katas diárias
2. **Baby steps** - Pequenas iterações
3. **Feedback rápido** - Execução de testes rápida
4. **Disciplina** - Não pule etapas
5. **Paciência** - Curva de aprendizado é íngreme no início

---

### FAQ

**P: TDD funciona para todos os projetos?**  
R: Não. É excelente para lógica de negócio, APIs. Menos para UI exploratória ou protótipos.

**P: Como começar com TDD?**  
R: Pratique com katas (String Calculator, Bowling Game, Tennis Refactoring) diariamente.

**P: TDD não ralent development?**  
R: No curto prazo parece mais lento, mas reduz significativamente bugs e tempo de debugging.

**P: Posso usar TDD com legacy code?**  
R: Sim! Comece com characterization tests, depois refatore一点点 com testes.
