<h2>O que é TDD e por que você deveria se importar</h2>
<p>Test-Driven Development (TDD) é uma metodologia onde você escreve testes <em>antes</em> de escrever o código de produção. Parece contraditório, mas a lógica é simples: se você não consegue escrever um teste que falha para uma funcionalidade, é porque você não sabe exatamente o que precisa implementar. Essa prática força você a pensar no comportamento esperado antes de codificar.</p>
<p>A adoção de TDD traz benefícios concretos. Seu código fica mais modular porque precisa ser testável. Você ganha confiança para refatorar sem medo de quebrar coisas. A documentação fica viva — os testes mostram exemplos reais de como usar o código. E o mais importante: você detecta bugs antes de colocá-los em produção. Não é sobre ter 100% de cobertura de testes; é sobre colocar a qualidade como prioridade desde o primeiro commit.</p>
<h2>O Ciclo Red-Green-Refactor</h2>
<p>O coração de TDD é um ciclo de três fases que você repete continuamente. Entender e praticar esse ciclo é fundamental para dominar a metodologia.</p>
<h3>Red: Escrever o teste que falha</h3>
<p>Você começa escrevendo um teste para uma funcionalidade que <em>ainda não existe</em>. O teste deve falhar — é por isso que chamamos de "Red". O teste descreve o comportamento esperado em linguagem executável. Esse passo força você a pensar na interface do seu código antes de implementá-lo.</p>
<h3>Green: Fazer o teste passar com o mínimo de código</h3>
<p>Agora você escreve o código de produção. O objetivo aqui não é perfeição; é fazer o teste passar do jeito mais simples possível. Você pode usar atalhos, hardcoding, qualquer coisa — desde que o teste fique verde. Isso pode parecer estranho, mas há um propósito: você se foca apenas na lógica que resolve o problema.</p>
<h3>Refactor: Melhorar o código sem quebrar testes</h3>
<p>Com o teste passando, você pode refatorar com segurança. Remova duplicação, melhore nomes de variáveis, simplify a lógica. Os testes garantem que você não quebrou nada. É aqui que o código ganha qualidade e elegância.</p>
<p>Esse ciclo é iterativo. Você não escreve todos os testes de uma vez. Para cada pequeno pedaço de funcionalidade — uma função, um método — você executa Red-Green-Refactor. Isso torna o desenvolvimento incremental e controlado.</p>
<h2>Exemplo Prático: Uma Calculadora Simples</h2>
<p>Vamos aplicar TDD desenvolvendo uma calculadora que realiza operações básicas. Vou mostrar o ciclo completo para que você veja como funciona na prática.</p>
<h3>Red: Escrevendo o primeiro teste</h3>
<pre><code class="language-python"># test_calculadora.py
import pytest
from calculadora import Calculadora
def test_somar_dois_numeros():
calc = Calculadora()
resultado = calc.somar(2, 3)
assert resultado == 5</code></pre>
<p>Se você rodar <code>pytest test_calculadora.py</code> agora, vai falhar — o módulo <code>calculadora</code> nem existe. Esse é o estado Red.</p>
<h3>Green: Implementação mínima</h3>
<pre><code class="language-python"># calculadora.py
class Calculadora:
def somar(self, a, b):
return a + b</code></pre>
<p>Agora <code>pytest test_calculadora.py</code> passa. Simples? Demais. Mas é proposital — você escreveu o mínimo. Vamos adicionar mais funcionalidades.</p>
<h3>Expandindo com novos testes</h3>
<pre><code class="language-python"># test_calculadora.py (expandido)
import pytest
from calculadora import Calculadora
def test_somar_dois_numeros():
calc = Calculadora()
resultado = calc.somar(2, 3)
assert resultado == 5
def test_somar_numeros_negativos():
calc = Calculadora()
resultado = calc.somar(-5, 3)
assert resultado == -2
def test_subtrair_dois_numeros():
calc = Calculadora()
resultado = calc.subtrair(10, 3)
assert resultado == 7
def test_multiplicar_dois_numeros():
calc = Calculadora()
resultado = calc.multiplicar(4, 5)
assert resultado == 20
def test_divisao_por_zero_lanca_erro():
calc = Calculadora()
with pytest.raises(ValueError):
calc.dividir(10, 0)
def test_dividir_dois_numeros():
calc = Calculadora()
resultado = calc.dividir(10, 2)
assert resultado == 5.0</code></pre>
<p>Agora temos vários testes falhando. Vamos ao Green.</p>
<h3>Green: Implementação completa</h3>
<pre><code class="language-python"># calculadora.py (expandido)
class Calculadora:
def somar(self, a, b):
return a + b
def subtrair(self, a, b):
return a - b
def multiplicar(self, a, b):
return a * b
def dividir(self, a, b):
if b == 0:
raise ValueError("Não é permitido dividir por zero")
return a / b</code></pre>
<p>Todos os testes passam. Está funcional, mas não está otimizado.</p>
<h3>Refactor: Melhorando a estrutura</h3>
<p>Agora vamos refatorar. Observe que temos repetição de <code>calc = Calculadora()</code> em cada teste. Usamos fixtures do pytest:</p>
<pre><code class="language-python"># test_calculadora.py (refatorado)
import pytest
from calculadora import Calculadora
@pytest.fixture
def calc():
"""Fixture que fornece uma instância de Calculadora para cada teste"""
return Calculadora()
def test_somar_dois_numeros(calc):
assert calc.somar(2, 3) == 5
def test_somar_numeros_negativos(calc):
assert calc.somar(-5, 3) == -2
def test_subtrair_dois_numeros(calc):
assert calc.subtrair(10, 3) == 7
def test_multiplicar_dois_numeros(calc):
assert calc.multiplicar(4, 5) == 20
def test_divisao_por_zero_lanca_erro(calc):
with pytest.raises(ValueError, match="Não é permitido dividir por zero"):
calc.dividir(10, 0)
def test_dividir_dois_numeros(calc):
assert calc.dividir(10, 2) == 5.0</code></pre>
<p>Melhor. Os testes continuam passando, mas estão mais limpos. Esse é o Refactor.</p>
<h2>Armadilhas Comuns e Como Evitá-las</h2>
<h3>Teste muito vago ou muito específico</h3>
<p>Um teste vago não verifica nada de valor. Um teste muito específico quebra com mudanças irrelevantes. O equilíbrio é testar o comportamento, não a implementação.</p>
<pre><code class="language-python"></code></pre>
<h3>Não testar casos extremos</h3>
<p>Testes devem cobrir caminhos felizes e caminhos de erro. Números negativos, zero, valores muito grandes — tudo isso importa.</p>
<pre><code class="language-python"></code></pre>
<h3>Testes acoplados a detalhes de implementação</h3>
<p>Se mudar a implementação, os testes não devem quebrar enquanto o comportamento permanece igual.</p>
<pre><code class="language-python"></code></pre>
<h2>Exemplo Avançado: Testando uma Classe com Estado</h2>
<p>Vamos aplicar TDD em algo mais realista — uma classe que gerencia uma conta bancária. Isso envolve estado, validações e regras de negócio.</p>
<h3>Começando com os testes</h3>
<pre><code class="language-python"># test_conta_bancaria.py
import pytest
from datetime import datetime
from conta_bancaria import ContaBancaria, SaldoInsuficiente
@pytest.fixture
def conta():
return ContaBancaria(titular="João Silva", saldo_inicial=1000.0)
def test_criar_conta_com_saldo_inicial(conta):
assert conta.saldo == 1000.0
assert conta.titular == "João Silva"
def test_deposito_aumenta_saldo(conta):
conta.depositar(500.0)
assert conta.saldo == 1500.0
def test_saque_diminui_saldo(conta):
conta.sacar(300.0)
assert conta.saldo == 700.0
def test_saque_com_saldo_insuficiente_lanca_erro(conta):
with pytest.raises(SaldoInsuficiente):
conta.sacar(2000.0)
def test_saldo_nao_pode_ser_negativo(conta):
assert conta.saldo >= 0
def test_historico_registra_transacoes(conta):
conta.depositar(100.0)
conta.sacar(50.0)
historico = conta.obter_historico()
assert len(historico) == 2
assert historico[0]["tipo"] == "deposito"
assert historico[0]["valor"] == 100.0
assert historico[1]["tipo"] == "saque"
assert historico[1]["valor"] == 50.0
def test_taxa_juros_aplicada_mensalmente(conta):
1000 + 1% = 1010
conta.aplicar_juros_mensais(taxa=0.01)
assert conta.saldo == 1010.0</code></pre>
<h3>Implementação seguindo TDD</h3>
<pre><code class="language-python"># conta_bancaria.py
from datetime import datetime
from typing import List, Dict
class SaldoInsuficiente(Exception):
"""Exceção levantada quando há tentativa de saque sem saldo suficiente"""
pass
class ContaBancaria:
def __init__(self, titular: str, saldo_inicial: float = 0.0):
self.titular = titular
self.saldo = saldo_inicial
self._historico: List[Dict] = []
if saldo_inicial > 0:
self._historico.append({
"tipo": "abertura",
"valor": saldo_inicial,
"data": datetime.now(),
"saldo_anterior": 0.0,
"saldo_novo": saldo_inicial
})
def depositar(self, valor: float) -> None:
"""Aumenta o saldo da conta"""
if valor <= 0:
raise ValueError("Depósito deve ser um valor positivo")
saldo_anterior = self.saldo
self.saldo += valor
self._historico.append({
"tipo": "deposito",
"valor": valor,
"data": datetime.now(),
"saldo_anterior": saldo_anterior,
"saldo_novo": self.saldo
})
def sacar(self, valor: float) -> None:
"""Diminui o saldo da conta"""
if valor <= 0:
raise ValueError("Saque deve ser um valor positivo")
if valor > self.saldo:
raise SaldoInsuficiente(
f"Saldo insuficiente. Saldo: {self.saldo}, Tentativa: {valor}"
)
saldo_anterior = self.saldo
self.saldo -= valor
self._historico.append({
"tipo": "saque",
"valor": valor,
"data": datetime.now(),
"saldo_anterior": saldo_anterior,
"saldo_novo": self.saldo
})
def obter_historico(self) -> List[Dict]:
"""Retorna o histórico de transações"""
return self._historico.copy()
def aplicar_juros_mensais(self, taxa: float) -> None:
"""Aplica juros ao saldo existente"""
if taxa < 0:
raise ValueError("Taxa de juros não pode ser negativa")
saldo_anterior = self.saldo
self.saldo = self.saldo * (1 + taxa)
self._historico.append({
"tipo": "juros",
"valor": self.saldo - saldo_anterior,
"data": datetime.now(),
"saldo_anterior": saldo_anterior,
"saldo_novo": self.saldo
})</code></pre>
<h3>Executando os testes</h3>
<pre><code class="language-bash">$ pytest test_conta_bancaria.py -v
test_conta_bancaria.py::test_criar_conta_com_saldo_inicial PASSED
test_conta_bancaria.py::test_deposito_aumenta_saldo PASSED
test_conta_bancaria.py::test_saque_diminui_saldo PASSED
test_conta_bancaria.py::test_saque_com_saldo_insuficiente_lanca_erro PASSED
test_conta_bancaria.py::test_saldo_nao_pode_ser_negativo PASSED
test_conta_bancaria.py::test_historico_registra_transacoes PASSED
test_conta_bancaria.py::test_taxa_juros_aplicada_mensalmente PASSED
======================== 7 passed in 0.05s ========================</code></pre>
<p>Todos passam. Agora temos confiança que o código funciona conforme esperado. Se você modificar a implementação depois, os testes garantem que o comportamento permanece correto.</p>
<h2>Ferramentas e Ambiente</h2>
<p>Para praticar TDD em Python, você precisa de poucos elementos, mas com as escolhas certas.</p>
<h3>pytest: O framework mais utilizado</h3>
<p>O pytest é o padrão de facto para testes em Python. É simples de começar, mas poderoso o bastante para casos complexos. A sintaxe é limpa — use <code>assert</code> direto, não precisa de métodos especiais como em outras linguagens.</p>
<pre><code class="language-bash">pip install pytest</code></pre>
<p>Para rodar os testes com mais verbosidade e ver qual falhou:</p>
<pre><code class="language-bash">pytest test_calculadora.py -v --tb=short</code></pre>
<h3>Coverage: Medindo cobertura de código</h3>
<p>Cobertura de testes mostra qual percentual do seu código está coberto por testes. Não é uma métrica perfeita, mas ajuda a identificar partes não testadas.</p>
<pre><code class="language-bash">pip install pytest-cov
pytest --cov=calculadora test_calculadora.py</code></pre>
<h3>Estrutura recomendada para seu projeto</h3>
<pre><code>projeto/
├── src/
│ └── calculadora.py
├── tests/
│ ├── __init__.py
│ ├── test_calculadora.py
│ └── test_conta_bancaria.py
├── pytest.ini
└── requirements-dev.txt</code></pre>
<h2>Conclusão</h2>
<p>TDD é uma mudança de mentalidade, não apenas uma técnica. Quando você escreve testes primeiro, você pensa diferente — seu código fica mais simples, mais testável, e consequentemente mais confiável. O ciclo Red-Green-Refactor é seu aliado. Comece pequeno: pegue uma funcionalidade simples e pratique o ciclo completo. Com o tempo, escrever testes primeiro vira natural.</p>
<p>Os três pontos principais que você leva daqui: Primeiro, TDD força você a pensar no design do código antes de implementar — isso resulta em APIs melhores. Segundo, testes funcionam como documentação viva e executável do comportamento esperado. Terceiro, o Refactor seguro é talvez o maior benefício — você muda o código com confiança porque os testes têm suas costas.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://docs.pytest.org/" target="_blank" rel="noopener noreferrer">pytest official documentation</a></li>
<li><a href="https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530" target="_blank" rel="noopener noreferrer">Test Driven Development: By Example - Kent Beck</a></li>
<li><a href="https://realpython.com/python-testing/" target="_blank" rel="noopener noreferrer">Real Python - Getting Started With Testing</a></li>
<li><a href="https://martinfowler.com/bliki/TestDrivenDevelopment.html" target="_blank" rel="noopener noreferrer">Martin Fowler - Test Driven Development</a></li>
<li><a href="https://docs.python.org/3/library/unittest.html" target="_blank" rel="noopener noreferrer">Python unittest official documentation</a></li>
</ul>
<p><!-- FIM --></p>