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{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442} menos | Baseline |
| Cobertura de testes | 90-100{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442} | 20-50{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442} |
| Confiança em refactoring | Alta | Baixa |
| Tempo de debugging | 30-50{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442} menos | Baseline |
| Custo de manutenção | 40{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442} menor | Baseline |
Quando Usar TDD
TDD BRILHA quando:
- Algoritmos complexos
- APIs e bibliotecas
- Refactoring de código legado
- Código compartilhado
- Requisitos claros
- UI layouts complexos
- Exploratory features
- Requisitos muito instáveis
- Prototipagem rápida
TDD é mais difícil quando:
TDD em Diferentes Linguagens
Python com pytest
# 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
# 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{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442} 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{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442} 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{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442}"""
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{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442}
(100, 20, 80), # 20{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442}
(100, 50, 70), # 30{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442}
(200, 10, 180), # 10{6727d158e4474b0847515bd23a8ee4bebd5f1aac7a18bc968e51d8985dccb442} 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
# 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
# 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
// 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
// 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
# 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
# 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 |
# 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
- Criar calculadora que recebe string vazia retorna 0
- Recebe “1” retorna 1
- Recebe “1,2” retorna 3
- Recebe “1n2,3” retorna 6
- Suporta delimitador customizado “//;n1;2” retorna 3
Solução TDD
# 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:
- Prática constante – Katas diárias
- Baby steps – Pequenas iterações
- Feedback rápido – Execução de testes rápida
- Disciplina – Não pule etapas
- 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.
