Desenvolvimento orientado a testes (TDD)
1. Fundamentos do TDD
O Desenvolvimento Orientado a Testes (TDD) é uma prática de engenharia de software que inverte a ordem tradicional de desenvolvimento. Em vez de escrever o código primeiro e depois testá-lo, no TDD o teste é escrito antes do código de produção. Essa abordagem foi popularizada por Kent Beck no final dos anos 1990 e refinada por Robert C. Martin (Uncle Bob) com suas "Três Leis do TDD".
O ciclo fundamental do TDD é conhecido como Red-Green-Refactor:
- Red: Escreva um teste que falhe (vermelho)
- Green: Escreva o código mínimo necessário para fazer o teste passar (verde)
- Refactor: Melhore o código mantendo todos os testes verdes
As Três Leis do TDD de Robert C. Martin estabelecem:
- Não escreva código de produção sem antes escrever um teste que falhe
- Não escreva mais de um teste unitário do que o suficiente para falhar (e falhas de compilação contam como falhas)
- Não escreva mais código de produção do que o suficiente para fazer o teste falhado passar
A principal diferença entre TDD e testes tradicionais é que no TDD o teste funciona como ferramenta de design, não apenas como verificação. O teste força o desenvolvedor a pensar na interface da API antes da implementação.
2. Configurando o Ambiente para TDD
Para começar com TDD em Python, utilizaremos o framework pytest. A configuração básica envolve:
# Estrutura de diretórios recomendada
meu_projeto/
├── src/
│ └── calculadora.py
├── tests/
│ └── test_calculadora.py
├── requirements.txt
└── pytest.ini
Instalação do ambiente:
pip install pytest pytest-watch coverage
Arquivo pytest.ini:
[pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*
3. Escrevendo o Primeiro Teste (Red)
Vamos criar um teste para uma função somar(a, b). O teste deve especificar o comportamento esperado:
# tests/test_calculadora.py
def test_somar_dois_numeros_positivos():
"""Cenário: somar 2 e 3 deve retornar 5"""
from src.calculadora import somar
resultado = somar(2, 3)
assert resultado == 5, f"Esperado 5, mas obteve {resultado}"
Execute o teste para ver a falha (Red):
pytest tests/test_calculadora.py
# Saída esperada:
# FAILED tests/test_calculadora.py::test_somar_dois_numeros_positivos -
# ImportError: cannot import name 'somar' from 'src.calculadora'
4. Implementando o Mínimo para Passar (Green)
Primeiro, criamos uma implementação mínima que faz o teste passar:
# src/calculadora.py
def somar(a, b):
return 5 # Falso verde: retorna valor constante
Execute novamente:
pytest tests/test_calculadora.py
# Saída esperada:
# 1 passed in 0.02s
Agora, substituímos o falso verde pela implementação real:
# src/calculadora.py
def somar(a, b):
return a + b
5. Refatoração Segura
Com os testes passando, podemos refatorar com segurança. Exemplo de identificação de duplicação:
# Antes da refatoração
def somar(a, b):
return a + b
def subtrair(a, b):
return a - b
def multiplicar(a, b):
return a * b
# Depois da refatoração - extraindo validação comum
def _validar_numeros(*args):
for arg in args:
if not isinstance(arg, (int, float)):
raise TypeError(f"Argumento {arg} não é numérico")
def somar(a, b):
_validar_numeros(a, b)
return a + b
def subtrair(a, b):
_validar_numeros(a, b)
return a - b
Execute os testes novamente para confirmar que continuam passando:
pytest tests/
6. TDD em Cenários Reais
Lidando com dependências externas usando mocks
# tests/test_servico.py
from unittest.mock import Mock
def test_servico_envia_email():
# Arrange
repositorio_mock = Mock()
repositorio_mock.buscar_usuario.return_value = {"nome": "João", "email": "joao@email.com"}
servico_email_mock = Mock()
from src.servico import ServicoNotificacao
servico = ServicoNotificacao(repositorio_mock, servico_email_mock)
# Act
servico.notificar_usuario(1, "Bem-vindo!")
# Assert
servico_email_mock.enviar.assert_called_once_with(
"joao@email.com",
"Bem-vindo!"
)
Testes para exceções e casos de borda
# tests/test_calculadora.py
import pytest
def test_somar_com_string_lanca_type_error():
from src.calculadora import somar
with pytest.raises(TypeError, match="não é numérico"):
somar("2", 3)
def test_somar_com_valores_extremos():
from src.calculadora import somar
resultado = somar(10**100, 10**100)
assert resultado == 2 * 10**100
def test_somar_com_zero():
from src.calculadora import somar
assert somar(0, 5) == 5
assert somar(5, 0) == 5
TDD em APIs web
# tests/test_api.py
def test_rota_soma_retorna_status_200():
from src.api import app
from fastapi.testclient import TestClient
client = TestClient(app)
response = client.post("/somar", json={"a": 3, "b": 4})
assert response.status_code == 200
assert response.json() == {"resultado": 7}
def test_rota_soma_com_dados_invalidos():
from src.api import app
from fastapi.testclient import TestClient
client = TestClient(app)
response = client.post("/somar", json={"a": "invalido", "b": 4})
assert response.status_code == 422
7. Integração Contínua e TDD
Pipeline de CI/CD com GitHub Actions:
# .github/workflows/test.yml
name: Testes TDD
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Configurar Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Instalar dependências
run: |
pip install pytest pytest-cov
- name: Executar testes com cobertura
run: |
pytest tests/ --cov=src/ --cov-report=term-missing
8. Armadilhas e Boas Práticas
Erros comuns a evitar:
- Testes acoplados à implementação: Teste comportamentos, não métodos internos
- Pular o passo Red: Escrever o teste depois do código quebra o propósito do TDD
- Esquecer de refatorar: O ciclo só se completa com a refatoração
Como manter testes rápidos e independentes:
# tests/conftest.py - Fixtures compartilhadas
import pytest
@pytest.fixture
def dados_usuario_padrao():
return {"id": 1, "nome": "João", "email": "joao@teste.com"}
# tests/test_usuario.py
def test_criar_usuario_com_dados_validos(dados_usuario_padrao):
from src.usuario import criar_usuario
usuario = criar_usuario(dados_usuario_padrao)
assert usuario.nome == "João"
assert usuario.email == "joao@teste.com"
Quando TDD não é adequado:
- Prototipação rápida para validação de conceito
- Código de UI altamente experimental
- Sistemas legados sem cobertura de testes (comece com testes de caracterização primeiro)
Referências
- Documentação oficial do pytest — Guia completo do framework de testes pytest, incluindo fixtures, mocks e plugins
- Test-Driven Development by Example (Kent Beck) — Livro fundamental que introduziu o conceito de TDD
- The Three Laws of TDD (Robert C. Martin) — Artigo clássico de Uncle Bob sobre as leis que regem o TDD
- pytest-mock documentation — Plugin pytest para mocks, essencial para testar dependências externas
- GitHub Actions Documentation - Testing Python — Guia oficial para configurar pipelines de CI/CD com testes Python
- Coverage.py documentation — Ferramenta para medir cobertura de código, complementar ao TDD
- FastAPI Testing Documentation — Como aplicar TDD em APIs FastAPI com TestClient