Python

Dominando TDD em Python: Desenvolvendo Guiado por Testes na Prática em Projetos Reais

14 min de leitura

Dominando TDD em Python: Desenvolvendo Guiado por Testes na Prática em Projetos Reais

O que é TDD e por que você deveria se importar Test-Driven Development (TDD) é uma metodologia onde você escreve testes antes 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. 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. O Ciclo Red-Green-Refactor O coração de TDD é um ciclo de três fases que você repete continuamente. Entender

<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 &quot;Red&quot;. 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(&quot;Não é permitido dividir por zero&quot;)

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():

&quot;&quot;&quot;Fixture que fornece uma instância de Calculadora para cada teste&quot;&quot;&quot;

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=&quot;Não é permitido dividir por zero&quot;):

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=&quot;João Silva&quot;, saldo_inicial=1000.0)

def test_criar_conta_com_saldo_inicial(conta):

assert conta.saldo == 1000.0

assert conta.titular == &quot;João Silva&quot;

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 &gt;= 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][&quot;tipo&quot;] == &quot;deposito&quot;

assert historico[0][&quot;valor&quot;] == 100.0

assert historico[1][&quot;tipo&quot;] == &quot;saque&quot;

assert historico[1][&quot;valor&quot;] == 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):

&quot;&quot;&quot;Exceção levantada quando há tentativa de saque sem saldo suficiente&quot;&quot;&quot;

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 &gt; 0:

self._historico.append({

&quot;tipo&quot;: &quot;abertura&quot;,

&quot;valor&quot;: saldo_inicial,

&quot;data&quot;: datetime.now(),

&quot;saldo_anterior&quot;: 0.0,

&quot;saldo_novo&quot;: saldo_inicial

})

def depositar(self, valor: float) -&gt; None:

&quot;&quot;&quot;Aumenta o saldo da conta&quot;&quot;&quot;

if valor &lt;= 0:

raise ValueError(&quot;Depósito deve ser um valor positivo&quot;)

saldo_anterior = self.saldo

self.saldo += valor

self._historico.append({

&quot;tipo&quot;: &quot;deposito&quot;,

&quot;valor&quot;: valor,

&quot;data&quot;: datetime.now(),

&quot;saldo_anterior&quot;: saldo_anterior,

&quot;saldo_novo&quot;: self.saldo

})

def sacar(self, valor: float) -&gt; None:

&quot;&quot;&quot;Diminui o saldo da conta&quot;&quot;&quot;

if valor &lt;= 0:

raise ValueError(&quot;Saque deve ser um valor positivo&quot;)

if valor &gt; self.saldo:

raise SaldoInsuficiente(

f&quot;Saldo insuficiente. Saldo: {self.saldo}, Tentativa: {valor}&quot;

)

saldo_anterior = self.saldo

self.saldo -= valor

self._historico.append({

&quot;tipo&quot;: &quot;saque&quot;,

&quot;valor&quot;: valor,

&quot;data&quot;: datetime.now(),

&quot;saldo_anterior&quot;: saldo_anterior,

&quot;saldo_novo&quot;: self.saldo

})

def obter_historico(self) -&gt; List[Dict]:

&quot;&quot;&quot;Retorna o histórico de transações&quot;&quot;&quot;

return self._historico.copy()

def aplicar_juros_mensais(self, taxa: float) -&gt; None:

&quot;&quot;&quot;Aplica juros ao saldo existente&quot;&quot;&quot;

if taxa &lt; 0:

raise ValueError(&quot;Taxa de juros não pode ser negativa&quot;)

saldo_anterior = self.saldo

self.saldo = self.saldo * (1 + taxa)

self._historico.append({

&quot;tipo&quot;: &quot;juros&quot;,

&quot;valor&quot;: self.saldo - saldo_anterior,

&quot;data&quot;: datetime.now(),

&quot;saldo_anterior&quot;: saldo_anterior,

&quot;saldo_novo&quot;: 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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

O que Todo Dev Deve Saber sobre Web Scraping em Python: requests, BeautifulSoup e Selenium
O que Todo Dev Deve Saber sobre Web Scraping em Python: requests, BeautifulSoup e Selenium

O que é Web Scraping e Por Que Aprender Web scraping é a técnica de extrair d...

Dominando asyncio Avançado em Python: Semáforos, Locks e Padrões de Concorrência em Projetos Reais
Dominando asyncio Avançado em Python: Semáforos, Locks e Padrões de Concorrência em Projetos Reais

Introdução: O Problema da Concorrência Controlada Quando trabalhamos com em P...

pip, virtualenv e venv em Python: Isolamento de Dependências: Do Básico ao Avançado
pip, virtualenv e venv em Python: Isolamento de Dependências: Do Básico ao Avançado

O Problema do Caos de Dependências Quando começamos a trabalhar com Python, e...