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

Testes Automatizados · 6 de julho de 2026

📖 8 min de leitura

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
    1. APIs e bibliotecas
    2. Refactoring de código legado
    3. Código compartilhado
    4. Requisitos claros

    TDD é mais difícil quando:

    1. UI layouts complexos
    2. Exploratory features
    3. Requisitos muito instáveis
    4. Prototipagem rápida


    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:

    1. Isolar unidade sob teste
    2. Simular dependências externas
    3. Controlar cenários de teste
    4. 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

  1. Criar calculadora que recebe string vazia retorna 0
  2. Recebe “1” retorna 1
  3. Recebe “1,2” retorna 3
  4. Recebe “1n2,3” retorna 6
  5. 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:

  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.