Testes Unitários em Python: Guia Completo com Exemplos
Testes em Python com pytest, TDD, mocking e coverage
Testes unitários garantem que seu código Python funcione corretamente e continue funcionando conforme seu projeto evolui. Este guia abrangente aborda tudo o que você precisa saber sobre testes unitários em Python, desde conceitos básicos até técnicas avançadas.

Por que os testes unitários importam
Os testes unitários oferecem diversos benefícios para desenvolvedores Python:
- Detecção precoce de bugs: Capturar bugs antes que cheguem à produção
- Qualidade do código: Força você a escrever código modular e testável
- Confiança ao refatorar: Faça alterações com segurança, sabendo que os testes capturarão regressões
- Documentação: Os testes servem como documentação executável de como o código deve funcionar
- Desenvolvimento mais rápido: Testes automatizados são mais rápidos do que testes manuais
- Melhor design: Escrever código testável leva a uma arquitetura melhor
Entendendo os fundamentos dos testes unitários
O que é um teste unitário?
Um teste unitário verifica a parte mais pequena e testável de uma aplicação (geralmente uma função ou método) em isolamento. Ele deve ser:
- Rápido: Executar em milissegundos
- Isolado: Independente de outros testes e sistemas externos
- Repetível: Produzir os mesmos resultados toda vez
- Autovalidável: Passar ou falhar claramente sem inspeção manual
- Oportuno: Escrito antes ou junto com o código
A pirâmide de testes
Um conjunto de testes saudável segue a pirâmide de testes:
/\
/ \ Testes E2E (Poucos)
/____\
/ \ Testes de Integração (Alguns)
/_____ ___\
/ \ Testes Unitários (Muitos)
/_____ __ ___\
Os testes unitários formam a base - são numerosos, rápidos e fornecem feedback rápido.
Comparação de frameworks de testes para Python
unittest: O framework embutido
A biblioteca padrão do Python inclui unittest, inspirado em JUnit:
import unittest
class TestCalculator(unittest.TestCase):
def setUp(self):
"""Executar antes de cada teste"""
self.calc = Calculator()
def tearDown(self):
"""Executar depois de cada teste"""
self.calc = None
def test_addition(self):
result = self.calc.add(2, 3)
self.assertEqual(result, 5)
def test_division_by_zero(self):
with self.assertRaises(ZeroDivisionError):
self.calc.divide(10, 0)
if __name__ == '__main__':
unittest.main()
Vantagens:
- Embutido, não é necessário instalar
- Bom para desenvolvedores familiarizados com frameworks xUnit
- Amigável para empresas, bem estabelecido
Desvantagens:
- Sintaxe verbosa com código repetitivo
- Requer classes para organização de testes
- Menos flexibilidade na gestão de fixtures
pytest: A escolha moderna
pytest é o framework de testes de terceiros mais popular:
import pytest
def test_addition():
calc = Calculator()
assert calc.add(2, 3) == 5
def test_division_by_zero():
calc = Calculator()
with pytest.raises(ZeroDivisionError):
calc.divide(10, 0)
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
])
def test_addition_parametrized(a, b, expected):
calc = Calculator()
assert calc.add(a, b) == expected
Vantagens:
- Sintaxe simples e Pythonica
- Sistema poderoso de fixtures
- Excelente ecossistema de plugins
- Relatórios de erros melhores
- Testes parametrizados embutidos
Desvantagens:
- Requer instalação
- Menos familiar para desenvolvedores de outras linguagens
Instalação:
pip install pytest pytest-cov pytest-mock
Escrevendo seus primeiros testes unitários
Vamos construir um exemplo simples do zero usando Desenvolvimento Guiado por Testes (TDD). Se você é novo em Python ou precisa de um rápido referencial para sintaxe e recursos da linguagem, consulte nossa Folha de Dicas do Python para uma visão abrangente dos fundamentos do Python.
Exemplo: Funções de utilidade de string
Passo 1: Escreva o teste primeiro (Vermelho)
Crie test_string_utils.py:
import pytest
from string_utils import reverse_string, is_palindrome, count_vowels
def test_reverse_string():
assert reverse_string("hello") == "olleh"
assert reverse_string("") == ""
assert reverse_string("a") == "a"
def test_is_palindrome():
assert is_palindrome("racecar") == True
assert is_palindrome("hello") == False
assert is_palindrome("") == True
assert is_palindrome("A man a plan a canal Panama") == True
def test_count_vowels():
assert count_vowels("hello") == 2
assert count_vowels("HELLO") == 2
assert count_vowels("xyz") == 0
assert count_vowels("") == 0
Passo 2: Escreva o código mínimo para passar (Verde)
Crie string_utils.py:
def reverse_string(s: str) -> str:
"""Inverter uma string"""
return s[::-1]
def is_palindrome(s: str) -> bool:
"""Verificar se a string é um palíndromo (ignorando maiúsculas e espaços)"""
cleaned = ''.join(s.lower().split())
return cleaned == cleaned[::-1]
def count_vowels(s: str) -> int:
"""Contar vogais em uma string"""
return sum(1 for char in s.lower() if char in 'aeiou')
Passo 3: Executar os testes
pytest test_string_utils.py -v
Saída:
test_string_utils.py::test_reverse_string PASSED
test_string_utils.py::test_is_palindrome PASSED
test_string_utils.py::test_count_vowels PASSED
Técnicas avançadas de testes
Usando fixtures para configuração de testes
Fixtures fornecem configuração e limpeza reutilizáveis para testes:
import pytest
from database import Database
@pytest.fixture
def db():
"""Criar um banco de dados de teste"""
database = Database(":memory:")
database.create_tables()
yield database # Fornecer ao teste
database.close() # Limpeza após o teste
@pytest.fixture
def sample_users(db):
"""Adicionar usuários de amostra ao banco de dados"""
db.add_user("Alice", "alice@example.com")
db.add_user("Bob", "bob@example.com")
return db
def test_get_user(sample_users):
user = sample_users.get_user_by_email("alice@example.com")
assert user.name == "Alice"
def test_user_count(sample_users):
assert sample_users.count_users() == 2
Escopo de fixtures
Controle a vida útil das fixtures com escopos:
@pytest.fixture(scope="function") # Padrão: executar para cada teste
def func_fixture():
return create_resource()
@pytest.fixture(scope="class") # Uma vez por classe de teste
def class_fixture():
return create_resource()
@pytest.fixture(scope="module") # Uma vez por módulo
def module_fixture():
return create_resource()
@pytest.fixture(scope="session") # Uma vez por sessão de teste
def session_fixture():
return create_expensive_resource()
Mockando dependências externas
Use mock para isolar o código de dependências externas:
from unittest.mock import Mock, patch, MagicMock
import requests
class WeatherService:
def get_temperature(self, city):
response = requests.get(f"https://api.weather.com/{city}")
return response.json()["temp"]
# Teste com mock
def test_get_temperature():
service = WeatherService()
# Mockar a função requests.get
with patch('requests.get') as mock_get:
# Configurar resposta mockada
mock_response = Mock()
mock_response.json.return_value = {"temp": 72}
mock_get.return_value = mock_response
# Teste
temp = service.get_temperature("Boston")
assert temp == 72
# Verificar a chamada
mock_get.assert_called_once_with("https://api.weather.com/Boston")
Usando o plugin pytest-mock
pytest-mock fornece uma sintaxe mais limpa:
def test_get_temperature(mocker):
service = WeatherService()
# Mockar usando pytest-mock
mock_response = mocker.Mock()
mock_response.json.return_value = {"temp": 72}
mocker.patch('requests.get', return_value=mock_response)
temp = service.get_temperature("Boston")
assert temp == 72
Testes parametrizados
Teste múltiplos cenários de forma eficiente:
import pytest
@pytest.mark.parametrize("input,expected", [
("", True),
("a", True),
("ab", False),
("aba", True),
("racecar", True),
("hello", False),
])
def test_is_palindrome_parametrized(input, expected):
assert is_palindrome(input) == expected
@pytest.mark.parametrize("number,is_even", [
(0, True),
(1, False),
(2, True),
(-1, False),
(-2, True),
])
def test_is_even(number, is_even):
assert (number % 2 == 0) == is_even
Testando exceções e tratamento de erros
import pytest
def divide(a, b):
if b == 0:
raise ValueError("Não é possível dividir por zero")
return a / b
def test_divide_by_zero():
with pytest.raises(ValueError, match="Não é possível dividir por zero"):
divide(10, 0)
def test_divide_by_zero_with_message():
with pytest.raises(ValueError) as exc_info:
divide(10, 0)
assert "zero" in str(exc_info.value).lower()
# Testando que nenhuma exceção é lançada
def test_divide_success():
result = divide(10, 2)
assert result == 5.0
Testando código assíncrono
Testar código assíncrono é essencial para aplicações Python modernas, especialmente ao trabalhar com APIs, bancos de dados ou serviços de IA. Aqui está como testar funções assíncronas:
import pytest
import asyncio
async def fetch_data(url):
"""Função assíncrona para buscar dados"""
await asyncio.sleep(0.1) # Simular chamada de API
return {"status": "success"}
@pytest.mark.asyncio
async def test_fetch_data():
result = await fetch_data("https://api.example.com")
assert result["status"] == "success"
@pytest.mark.asyncio
async def test_fetch_data_with_mock(mocker):
# Mockar a função assíncrona
mock_fetch = mocker.AsyncMock(return_value={"status": "mocked"})
mocker.patch('module.fetch_data', mock_fetch)
result = await fetch_data("https://api.example.com")
assert result["status"] == "mocked"
Para exemplos práticos de testes de código assíncrono com serviços de IA, veja nosso guia sobre integrar Ollama com Python, que inclui estratégias de teste para interações com LLM.
Cobertura de código
Meça quanta parte do seu código está testada:
Usando pytest-cov
# Executar testes com cobertura
pytest --cov=myproject tests/
# Gerar relatório HTML
pytest --cov=myproject --cov-report=html tests/
# Mostrar linhas faltando
pytest --cov=myproject --cov-report=term-missing tests/
Configuração de cobertura
Criar .coveragerc:
[run]
source = myproject
omit =
*/tests/*
*/venv/*
*/__pycache__/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
@abstractmethod
Boas práticas de cobertura
- Almeje 80%+ de cobertura para caminhos críticos de código
- Não se preocupe com 100% - foque em testes significativos
- Teste casos de borda não apenas caminhos felizes
- Exclua código de boilerplate dos relatórios de cobertura
- Use a cobertura como um guia e não como um objetivo
Organização de testes e estrutura do projeto
Estrutura recomendada
myproject/
├── myproject/
│ ├── __init__.py
│ ├── module1.py
│ ├── module2.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Fixtures compartilhados
│ ├── test_module1.py
│ ├── test_module2.py
│ ├── test_utils.py
│ └── integration/
│ ├── __init__.py
│ └── test_integration.py
├── pytest.ini
├── requirements.txt
└── requirements-dev.txt
conftest.py para fixtures compartilhados
# tests/conftest.py
import pytest
from myproject.database import Database
@pytest.fixture(scope="session")
def test_db():
"""Banco de dados de teste para toda a sessão"""
db = Database(":memory:")
db.create_schema()
yield db
db.close()
@pytest.fixture
def clean_db(test_db):
"""Banco de dados limpo para cada teste"""
test_db.clear_all_tables()
return test_db
Configuração de pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--strict-markers
--cov=myproject
--cov-report=term-missing
--cov-report=html
markers =
slow: marca testes como lentos
integration: marca testes como testes de integração
unit: marca testes como testes unitários
Boas práticas para testes unitários
1. Siga o padrão AAA
Arrange-Act-Assert torna os testes claros:
def test_user_creation():
# Arrange
username = "john_doe"
email = "john@example.com"
# Act
user = User(username, email)
# Assert
assert user.username == username
assert user.email == email
assert user.is_active == True
2. Uma afirmação por teste (orientação, não regra)
# Bom: Teste focado
def test_user_username():
user = User("john_doe", "john@example.com")
assert user.username == "john_doe"
def test_user_email():
user = User("john_doe", "john@example.com")
assert user.email == "john@example.com"
# Também aceitável: Afirmações relacionadas
def test_user_creation():
user = User("john_doe", "john@example.com")
assert user.username == "john_doe"
assert user.email == "john@example.com"
assert isinstance(user.created_at, datetime)
3. Use nomes descritivos para testes
# Ruim
def test_user():
pass
# Bom
def test_user_creation_with_valid_data():
pass
def test_user_creation_fails_with_invalid_email():
pass
def test_user_password_is_hashed_after_setting():
pass
4. Teste casos de borda e limites
def test_age_validation():
# Casos válidos
assert validate_age(0) == True
assert validate_age(18) == True
assert validate_age(120) == True
# Casos de borda
assert validate_age(-1) == False
assert validate_age(121) == False
# Casos de borda
with pytest.raises(TypeError):
validate_age("18")
with pytest.raises(TypeError):
validate_age(None)
5. Mantenha os testes independentes
# Ruim: Testes dependem da ordem
counter = 0
def test_increment():
global counter
counter += 1
assert counter == 1
def test_increment_again(): # Falha se executado sozinho
global counter
counter += 1
assert counter == 2
# Bom: Testes são independentes
def test_increment():
counter = Counter()
counter.increment()
assert counter.value == 1
def test_increment_multiple_times():
counter = Counter()
counter.increment()
counter.increment()
assert counter.value == 2
6. Não teste detalhes de implementação
# Ruim: Testando implementação
def test_sort_uses_quicksort():
sorter = Sorter()
assert sorter.algorithm == "quicksort"
# Bom: Testando comportamento
def test_sort_returns_sorted_list():
sorter = Sorter()
result = sorter.sort([3, 1, 2])
assert result == [1, 2, 3]
7. Teste casos de uso reais
Quando testando bibliotecas que processam ou transformam dados, foque em cenários do mundo real. Por exemplo, se você estiver trabalhando com raspagem de web ou conversão de conteúdo, consulte nosso guia sobre conversão de HTML para Markdown com Python, que inclui estratégias de teste e comparações de benchmarks para diferentes bibliotecas de conversão.
Integração com Integração Contínua
Exemplo de GitHub Actions
# .github/workflows/tests.yml
name: Testes
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, 3.10, 3.11, 3.12]
steps:
- uses: actions/checkout@v3
- name: Configurar Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Instalar dependências
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Executar testes
run: |
pytest --cov=myproject --cov-report=xml
- name: Enviar cobertura
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
Testando funções serverless
Quando testando funções AWS Lambda ou aplicações serverless, considere estratégias de integração junto com testes unitários. Nosso guia sobre construindo uma AWS Lambda dual-mode com Python e Terraform aborda abordagens de teste para aplicações Python serverless, incluindo como testar manipuladores de Lambda, consumidores SQS e integrações com API Gateway.
Checklist de boas práticas de testes
- Escreva testes antes ou junto com o código (TDD)
- Mantenha os testes rápidos (< 1 segundo por teste)
- Faça os testes independentes e isolados
- Use nomes descritivos para testes
- Siga o padrão AAA (Arrange-Act-Assert)
- Teste casos de borda e condições de erro
- Mocke dependências externas
- Almeje 80%+ de cobertura de código
- Execute os testes no pipeline CI/CD
- Revise e refatore os testes regularmente
- Documente cenários de testes complexos
- Use fixtures para configuração comum
- Parametrize testes semelhantes
- Mantenha os testes simples e legíveis
Padrões comuns de testes
Testando classes
class TestUser:
@pytest.fixture
def user(self):
return User("john_doe", "john@example.com")
def test_username(self, user):
assert user.username == "john_doe"
def test_email(self, user):
assert user.email == "john@example.com"
def test_full_name(self, user):
user.first_name = "John"
user.last_name = "Doe"
assert user.full_name() == "John Doe"
Testando com arquivos temporários
import pytest
from pathlib import Path
@pytest.fixture
def temp_file(tmp_path):
"""Criar um arquivo temporário"""
file_path = tmp_path / "test_file.txt"
file_path.write_text("test content")
return file_path
def test_read_file(temp_file):
content = temp_file.read_text()
assert content == "test content"
Testando geração de arquivos
Quando testando código que gera arquivos (como PDFs, imagens ou documentos), use diretórios temporários e verifique propriedades de arquivos:
@pytest.fixture
def temp_output_dir(tmp_path):
"""Fornece um diretório de saída temporário"""
output_dir = tmp_path / "output"
output_dir.mkdir()
return output_dir
def test_pdf_generation(temp_output_dir):
pdf_path = temp_output_dir / "output.pdf"
generate_pdf(pdf_path, content="Test")
assert pdf_path.exists()
assert pdf_path.stat().st_size > 0
Para exemplos abrangentes de testes de geração de PDF, veja nosso guia sobre gerando PDFs em Python, que aborda estratégias de teste para várias bibliotecas de PDF.
Testando com monkeypatch
def test_environment_variable(monkeypatch):
monkeypatch.setenv("API_KEY", "test_key_123")
assert os.getenv("API_KEY") == "test_key_123"
def test_module_attribute(monkeypatch):
monkeypatch.setattr("module.CONSTANT", 42)
assert module.CONSTANT == 42
Links úteis e recursos
- Documentação do pytest
- Documentação do unittest
- Plugin pytest-cov
- Plugin pytest-mock
- Documentação do unittest.mock
- Test Driven Development by Example - Kent Beck
- Python Testing with pytest - Brian Okken
Recursos relacionados
Fundamentos do Python e boas práticas
- Folha de Dicas do Python - Referência abrangente para sintaxe Python, estruturas de dados e padrões comuns
Testando casos específicos de uso do Python
- Integrando Ollama com Python - Estratégias de teste para integrações com LLM e chamadas assíncronas a serviços de IA
- Convertendo HTML para Markdown com Python - Abordagens de teste para raspagem de web e bibliotecas de conversão de conteúdo
- Gerando PDF em Python - Estratégias de teste para geração de PDF e saída de arquivos
Testes em ambientes serverless e na nuvem
- Construindo uma AWS Lambda dual-mode com Python e Terraform - Estratégias de teste para funções Lambda, consumidores SQS e integrações com API Gateway
Testes unitários são uma habilidade essencial para desenvolvedores Python. Seja qual for sua escolha entre unittest ou pytest, o ponto principal é escrever testes consistentemente, mantê-los mantiveis e integrá-los ao seu fluxo de trabalho de desenvolvimento. Comece com testes simples, gradualmente adote técnicas avançadas como mock e fixtures, e use ferramentas de cobertura para identificar código não testado.