Testes automatizados são fundamentais para garantir a qualidade e a confiabilidade do código Python. Entre as diversas ferramentas disponíveis, o pytest se destaca como a escolha preferida de desenvolvedores Python ao redor do mundo. Este guia abrangente vai mostrar tudo o que você precisa saber para dominar testes automatizados com pytest em seus projetos.
Por Que Utilizar pytest para Testes em Python?
O pytest é um framework de testes moderno que oferece uma abordagem simples e poderosa para escrever testes em Python. Diferente de outras ferramentas, o pytest minimaliza a quantidade de código necessária para escrever testes claros e eficientes. A filosofia do pytest é "testes simples e legíveis", tornando-o acessível tanto para iniciantes quanto para desenvolvedores experientes.
Entre as principais vantagens do pytest, podemos destacar sua sintaxe concisa que permite criar testes com menos linhas de código. A descoberta automática de testes é outro recurso poderoso, onde o framework encontra e executa testes sem necessidade de configuração complexa. Além disso, o pytest possui uma vasta coleção de plugins que estendem suas funcionalidades, permitindo integração com ferramentas de coverage, profiling e muito mais.
O pytest também oferece mensagens de erro extremamente detalhadas e úteis, facilitando significativamente o processo de debugging. Quando um teste falha, o pytest mostra exatamente qual asserção falhou e em qual linha do código, tornando a correção de bugs muito mais rápida e eficiente.
Instalação e Configuração do pytest
A instalação do pytest é extremamente simples e pode ser realizada através do pip, o gerenciador de pacotes Python. Execute o seguinte comando no seu terminal:
pip install pytest
Para verificar se a instalação foi bem sucedida, você pode executar:
pytest --version
Este comando deve retornar a versão do pytest instalada, confirmando que tudo está funcionando corretamente. É altamente recomendado criar um ambiente virtual específico para o seu projeto, isolar suas dependências e garantir que os testes executem de forma consistente em diferentes ambientes.
Caso precise de funcionalidades adicionais, você pode instalar plugins específicos. Por exemplo, para testes de cobertura de código, você pode instalar o pytest-cov:
pip install pytest-cov
Outro plugin muito útil é o pytest-xdist, que permite executar testes em paralelo, reduzindo significativamente o tempo de execução da sua suíte de testes:
pip install pytest-xdist
Sua Primeira Suite de Testes com pytest
Agora que o pytest está instalado, vamos criar nosso primeiro teste. O pytest segue convenções simples: arquivos de teste devem começar com "test_" ou terminar com "test.py". Funções de teste também devem começar com "test".
Vamos criar um arquivo de exemplo chamado test_matematica.py:
def somar(a, b):
return a + b
def subtrair(a, b):
return a - b
def multiplicar(a, b):
return a * b
def dividir(a, b):
if b == 0:
raise ValueError("Divisão por zero não é permitida")
return a / b
## Testes
def test_somar():
assert somar(2, 3) == 5
assert somar(-1, 1) == 0
assert somar(0, 0) == 0
def test_subtrair():
assert subtrair(5, 3) == 2
assert subtrair(10, 10) == 0
assert subtrair(-5, -3) == -2
def test_multiplicar():
assert multiplicar(3, 4) == 12
assert multiplicar(0, 5) == 0
assert multiplicar(-2, 3) == -6
def test_dividir():
assert dividir(10, 2) == 5
assert dividir(15, 3) == 5
assert dividir(100, 10) == 10
def test_dividir_por_zero():
with pytest.raises(ValueError):
dividir(10, 0)
Para executar os testes, basta rodar o comando pytest no diretório do seu projeto ou especificar o arquivo:
pytest test_matematica.py
O pytest会自动发现并运行所有以 "test_" 开头的函数,并显示详细的测试结果。输出将包括通过和失败的测试数量,以及失败测试的详细信息。
Asserções com pytest
O pytest oferece diversas formas de realizar asserções, sendo a mais direta o uso do comando assert. Quando uma asserção falha, o pytest fornece informações detalhadas sobre o erro, incluindo o valor esperado e o valor obtido.
Além do assert padrão, o pytest também fornece módulos auxiliares que oferecem asserções mais específicas. O módulo pytest contém diversas funções que facilitam testes comuns:
import pytest
def test_verificar_igualdade():
assert 5 == 5
def test_verificar_verdadeiro():
assert True
def test_verificar_falso():
assert not False
def test_verificar_none():
resultado = None
assert resultado is None
def test_verificar_contem():
lista = [1, 2, 3, 4, 5]
assert 3 in lista
def test_verificar_tamanho():
texto = "Python"
assert len(texto) == 6
def test_verificar_maior_menor():
assert 10 > 5
assert 3 < 8
Para asserções mais complexas, você pode usar as funções auxiliares do pytest. Por exemplo, pytest.approx() é útil para comparações de números flutuantes, onde você precisa lidar com imprecisões de ponto flutuante:
def test_calculo_decimal():
resultado = 0.1 + 0.2
assert resultado == pytest.approx(0.3, rel=1e-2)
Fixtures no pytest
Fixtures são uma das funcionalidades mais poderosas do pytest. Elas permitem criar dados de teste reutilizáveis e gerenciar dependências de forma elegante. Uma fixture é definida usando o decorador @pytest.fixture e pode ser injetada em qualquer função de teste.
Vamos ver um exemplo prático:
import pytest
from datetime import datetime
## Fixture simples que retorna um dicionário de usuário
@pytest.fixture
def usuario():
return {
'nome': 'João Silva',
'email': '[email protected]',
'idade': 30
}
## Fixture que cria uma conexão de banco de dados simulado
@pytest.fixture
def conexao_bd():
# Simula a conexão com um banco de dados
conexao = {
'conectado': False,
'dados': []
}
def executar_query(query):
conexao['dados'].append(query)
return ['resultado_simulado']
conexao['executar_query'] = executar_query
return conexao
## Testes utilizando as fixtures
def test_usuario_nome(usuario):
assert usuario['nome'] == 'João Silva'
def test_usuario_email(usuario):
assert '@' in usuario['email']
def test_conexao_bd(conexao_bd):
resultado = conexao_bd['executar_query']("SELECT * FROM usuarios")
assert len(resultado) > 0
assert len(conexao_bd['dados']) == 1
As fixtures podem ter diferentes escopos, determinando com que frequência são criadas. O escopo padrão é "function", meaning a fixture é criada uma vez por função de teste. Você pode alterar o escopo para "module", "class" ou "session" conforme necessário:
## Fixture de escopo de módulo - criada uma vez por arquivo de teste
@pytest.fixture(scope="module")
def configuracao_global():
return {
'ambiente': 'desenvolvimento',
'debug': True
}
## Fixture de escopo de classe - criada uma vez por classe de teste
@pytest.fixture(scope="class")
def banco_dados_teste():
# Setup
db = criar_banco_teste()
yield db
# Teardown
db.limpar()
O "teardown" é realizado automaticamente após o teste usando a palavra-chave yield. Isso é especialmente útil para limpar recursos após os testes, como fechar conexões de banco de dados ou arquivos.
Parametrize: Testando Múltiplos Cenários
O decorator @pytest.mark.parametrize é extremamente útil quando você precisa executar o mesmo teste com diferentes valores de entrada. Em vez de escrever múltiplas funções de teste, você pode usar parametrize para combinar todos os cenários em uma única função.
import pytest
@pytest.mark.parametrize("entrada,esperado", [
(2, 4), # 2 ao quadrado = 4
(3, 9), # 3 ao quadrado = 9
(5, 25), # 5 ao quadrado = 25
(10, 100), # 10 ao quadrado = 100
(0, 0), # 0 ao quadrado = 0
(-2, 4), # -2 ao quadrado = 4
])
def test_ao_quadrado(entrada, esperado):
assert entrada ** 2 == esperado
## Parametrize com múltiplos parâmetros
@pytest.mark.parametrize("a,b,operacao,resultado", [
(2, 3, 'somar', 5),
(10, 5, 'subtrair', 5),
(4, 6, 'multiplicar', 24),
(20, 4, 'dividir', 5),
(3, 3, 'potencia', 27),
])
def test_calculadora(a, b, operacao, resultado):
if operacao == 'somar':
assert a + b == resultado
elif operacao == 'subtrair':
assert a - b == resultado
elif operacao == 'multiplicar':
assert a * b == resultado
elif operacao == 'dividir':
assert a / b == resultado
elif operacao == 'potencia':
assert a ** b == resultado
Você também pode usar ids自定义 para identificar cada caso de teste:
@pytest.mark.parametrize("numero,esperado", [
pytest.param(1, 1, id="um"),
pytest.param(2, 2, id="dois"),
pytest.param(3, 6, id="tres_fatorial"),
], ids=["um", "dois", "tres"])
def test_exemplos_com_ids(numero, esperado):
assert numero == esperado
Marcadores e Organização de Testes
Os marcadores (markers) permitem categorizar e filtrar testes de acordo com diferentes critérios. Isso é especialmente útil em projetos grandes onde você precisa executar apenas um subconjunto de testes.
import pytest
## Marcadores personalizados
pytest.mark.lento
pytest.mark.rapido
pytest.mark.integracao
pytest.mark.unidade
pytest.mark.slow
pytest.mark.integration
## Usando marcadores nos testes
@pytest.mark.unidade
def test_calculo_simples():
assert 1 + 1 == 2
@pytest.mark.integracao
def test_conexao_banco():
# Este teste requer banco de dados real
pass
@pytest.mark.lento
def test_processamento_massa():
# Este teste leva muito tempo para executar
pass
## Marcando testes com skip e xfail
@pytest.mark.skip(reason="Funcionalidade ainda não implementada")
def test_proxima_feature():
pass
@pytest.mark.xfail(reason="Bug conhecido, será corrigido na próxima versão")
def test_funcionalidade_com_bug():
assert False
Para executar apenas testes de uma categoria específica, use o parâmetro -m:
## Executar apenas testes de unidade
pytest -m unidade
## Executar testes que não são lentos
pytest -m "not lento"
## Executar testes de integração
pytest -m integracao
Testes de Exceções
O pytest facilita a verificação de exceções usando pytest.raises(). Esta funcionalidade é essencial para testar código que deve lançar erros em situações específicas.
import pytest
def validar_idade(idade):
if idade < 0:
raise ValueError("Idade não pode ser negativa")
if idade > 150:
raise ValueError("Idade inválida")
return True
def test_idade_valida():
assert validar_idade(25) == True
def test_idade_negativa():
with pytest.raises(ValueError) as exc_info:
validar_idade(-5)
assert "negativa" in str(exc_info.value)
def test_idade_muito_alta():
with pytest.raises(ValueError) as exc_info:
validar_idade(200)
assert "inválida" in str(exc_info.value)
## Também podemos testar o tipo de exceção
def test_tipo_errado():
with pytest.raises(TypeError):
validar_idade("vinte e cinco") # Passando string em vez de número
Mocking e Patch
Quando você precisa testar código que depende de componentes externos (como APIs, bancos de dados ou sistemas de arquivos), o mocking é essencial. O pytest pode ser usado em conjunto com a biblioteca unittest.mock para criar mocks e stubs.
from unittest.mock import Mock, patch, MagicMock
import pytest
## Exemplo: Testando uma função que faz requisições HTTP
def buscar_dados_api(url):
import requests
resposta = requests.get(url)
return resposta.json()
@patch('requests.get')
def test_buscar_dados_api_com_sucesso(mock_get):
# Configurar o mock
mock_resposta = Mock()
mock_resposta.json.return_value = {'nome': 'João', 'idade': 30}
mock_get.return_value = mock_resposta
# Executar o teste
resultado = buscar_dados_api('https://api.exemplo.com/usuario')
# Verificar resultado
assert resultado == {'nome': 'João', 'idade': 30}
mock_get.assert_called_once_with('https://api.exemplo.com/usuario')
@patch('requests.get')
def test_buscar_dados_api_com_erro(mock_get):
mock_get.side_effect = Exception("Erro de conexão")
with pytest.raises(Exception) as exc_info:
buscar_dados_api('https://api.exemplo.com/usuario')
assert "conexão" in str(exc_info.value)
## Usando MagicMock para objetos mais complexos
@pytest.fixture
def mock_usuario():
usuario = MagicMock()
usuario.nome = "Maria"
usuario.email = "[email protected]"
usuario.obter_endereco.return_value = "Rua ABC, 123"
return usuario
def test_usuario_mock(mock_usuario):
assert mock_usuario.nome == "Maria"
assert mock_usuario.obter_endereco() == "Rua ABC, 123"
Cobertura de Código
Medir a cobertura de código é fundamental para garantir que seus testes estão realmente validando todas as linhas do seu código. O pytest-cov oferece essa funcionalidade.
## Executar testes com coverage
pytest --cov=meu_modulo --cov-report=html
## Ver cobertura detalhada por arquivo
pytest --cov=meu_modulo --cov-report=term-missing
Para configurar o coverage no seu arquivo pytest.ini ou pyproject.toml:
[tool.pytest.ini_options]
addopts = "--cov=src --cov-report=html --cov-report=term"
Configuração Avançada: pytest.ini e pyproject.toml
Você pode configurar o pytest usando arquivos de configuração para personalizar o comportamento padrão. Isso é especialmente útil em projetos grandes.
## pytest.ini
[pytest]
testpaths = testes
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short
markers =
lento: testes que demoram para executar
integracao: testes de integração
unidade: testes unitários
Ou usando pyproject.toml (recomendado para projetos modernos):
[tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["tests"]
pythonpath = ["src"]
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*", "*/venv/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if __name__ == .__main__.:",
]
Boas Práticas com pytest
Para escrever testes eficazes e mantíveis, siga estas boas práticas:
Nomenclatura clara: Nomeie seus testes de forma descritiva para que seja fácil entender o que cada teste verifica sem precisar ler o código:
## Bom
def test_calcular_media_deve_retornar_valor_correto_quando_lista_tiver_numeros():
pass
## Ruim
def test_media():
pass
Testes independentes: Cada teste deve ser capaz de executar independentemente dos outros. Evite dependências entre testes e limpe qualquer estado compartilhado:
@pytest.fixture(autouse=True)
def resetar_estado():
# Limpar estado antes de cada teste
EstadoGlobal.limpar()
yield
# Limpar após o teste também
EstadoGlobal.limpar()
Um assert por teste (opcional): Embora não seja uma regra rígida, muitos desenvolvedores preferem ter apenas uma asserção por teste para facilitar a identificação do problema quando um teste falha.
Use fixtures para dados reutilizáveis: Evite repetição de código criando fixtures para dados que são usados em vários testes.
Mantenha testes rápidos: Testes lentos tendem a ser executados com menos frequência. Se um teste é lento, considere refatorá-lo ou dividi-lo em testes menores.
Documente testes complexos: Quando a lógica do teste não for óbvia, adicione docstrings explicando o cenário sendo testado.
Executando Testes em Paralelo
Para projetos grandes com muitos testes, executar em paralelo pode reduzir significativamente o tempo de execução. O plugin pytest-xdist permite isso:
## Executar com 4 workers
pytest -n 4
## Executar com auto-detecção de CPU
pytest -n auto
Você também pode executar testes específicos:
## Executar um arquivo específico
pytest tests/test_usuario.py
## Executar uma função específica
pytest tests/test_usuario.py::test_criar_usuario
## Executar testes que correspondam a um padrão
pytest -k "test_usuario"
## Executar testes com marker específico
pytest -m "not lento"
Integração com CI/CD
O pytest se integra perfeitamente com pipelines de CI/CD como GitHub Actions, GitLab CI, Jenkins e outros. Aqui está um exemplo de configuração com GitHub Actions:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov
- name: Run tests
run: pytest --cov=src --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
Conclusão
O pytest é uma ferramenta essencial para qualquer desenvolvedor Python que leva a qualidade do código a sério. Com sua sintaxe simples, poderosa estrutura de fixtures e vasta ecosistema de plugins, o pytest torna a escrita e manutenção de testes uma tarefa muito mais agradável.
Dominar pytest não é apenas sobre aprender a sintaxe, mas entender como escrever testes que realmente agregam valor ao seu projeto. Tests bem escritos servem como documentação executável do seu código e protejem contra regressões futuras.
Para continuar aprendendo, explore a documentação oficial do pytest em https://docs.pytest.org e pratique com projetos reais. Quanto mais você escrever testes, mais natural se tornará o processo de desenvolvimento orientado a testes.
Lembre-se: testes automatizados são um investimento no futuro do seu projeto. O tempo investido hoje em escrever testes de qualidade será compensado muitas vezes ao longo da vida útil do seu código.
Links Externos
- Documentação Oficial do pytest - Documentação completa e oficial do framework pytest
- Python.org - Guia de Testes - Documentação oficial do Python sobre testes
- Real Python - pytest Tutorial - Tutorial completo de pytest em português
- DataCamp - pytest Course - Curso sobre desenvolvimento orientado a testes
- Python Testing - Guia abrangente sobre testes em Python
- Toptal - pytest Best Practices - Melhores práticas com pytest
- JetBrains - pytest Guide - Guia de pytest no PyCharm
- GitHub - pytest-dev - Repositório oficial do pytest no GitHub
- Codecov - Python Coverage - Ferramenta para coverage de código Python