No desenvolvimento de software, testar o tratamento de exceções e erros é extremamente importante. Realizar testes adequados pode melhorar a confiabilidade do código e prevenir falhas causadas por erros inesperados. Neste artigo, explicaremos em detalhes como testar eficientemente o tratamento de exceções e erros usando o pytest, um framework de testes para Python. Vamos abordar desde a configuração básica até os testes de exceções personalizadas e múltiplas exceções, de forma gradual.
Configuração Básica do pytest
O pytest é um poderoso framework de testes para Python, que pode ser facilmente instalado e utilizado. Siga os passos abaixo para configurar o pytest.
Instalação do pytest
Primeiro, é necessário instalar o pytest. Utilize o comando abaixo para instalar via pip.
pip install pytest
Estrutura Básica de Diretórios
Recomenda-se a seguinte estrutura de diretórios para o projeto de testes.
my_project/
├── src/
│ └── my_module.py
└── tests/
├── __init__.py
└── test_my_module.py
Criando o Primeiro Arquivo de Teste
Em seguida, crie um arquivo Python para testes. Por exemplo, crie um arquivo chamado test_my_module.py
no diretório tests
e adicione o seguinte conteúdo.
def test_example():
assert 1 + 1 == 2
Executando os Testes
Para executar os testes, rode o seguinte comando na raiz do projeto.
pytest
Isso fará com que o pytest detecte automaticamente os arquivos de teste no diretório tests
e execute-os. Se os testes forem bem-sucedidos, a configuração básica estará concluída.
Como Testar Exceções
O pytest oferece uma maneira conveniente de verificar se exceções são geradas corretamente. Aqui, explicaremos como testar se uma exceção específica é lançada.
Testando Exceções com pytest.raises
Para verificar se uma exceção é lançada, utilize o gerenciador de contexto pytest.raises
. No exemplo a seguir, testaremos se uma divisão por zero gera uma exceção.
import pytest
def test_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0
Esse teste verifica se a execução de 1 / 0
gera um ZeroDivisionError
.
Verificando a Mensagem da Exceção
Às vezes, é importante verificar não só se uma exceção é lançada, mas também se uma mensagem de erro específica está presente. Para isso, use o parâmetro match
.
def test_zero_division_message():
with pytest.raises(ZeroDivisionError, match="division by zero"):
1 / 0
Nesse teste, verificamos se um ZeroDivisionError
é lançado e se a mensagem de erro contém “division by zero”.
Testando Múltiplas Exceções
Também é possível testar múltiplas exceções em um único caso de teste, como quando diferentes condições geram diferentes exceções.
def test_multiple_exceptions():
with pytest.raises(ZeroDivisionError):
1 / 0
with pytest.raises(TypeError):
'1' + 1
Esse teste verifica primeiro se um ZeroDivisionError
é gerado e, em seguida, se um TypeError
é gerado.
Realizar testes de exceções de forma adequada garante que o código possa lidar corretamente com erros sem comportamentos inesperados.
Verificação de Mensagens de Erro
Verificar se mensagens de erro específicas estão presentes nos testes é importante. Vamos explicar como validar mensagens de erro usando pytest.
Confirmando Mensagens de Erro Específicas
Você pode testar se uma exceção contém uma mensagem de erro específica utilizando o pytest.raises
com o parâmetro match
.
import pytest
def test_value_error_message():
def raise_value_error():
raise ValueError("This is a ValueError with a specific message.")
with pytest.raises(ValueError, match="specific message"):
raise_value_error()
Neste teste, verificamos se um ValueError
é gerado e se sua mensagem contém “specific message”.
Validando Mensagens de Erro com Expressões Regulares
Quando a mensagem de erro é gerada dinamicamente ou você deseja verificar uma correspondência parcial, as expressões regulares são úteis.
def test_regex_error_message():
def raise_type_error():
raise TypeError("TypeError: invalid type for operation")
with pytest.raises(TypeError, match=r"invalid type"):
raise_type_error()
Neste teste, verificamos se a mensagem do TypeError
contém a frase “invalid type” usando uma expressão regular.
Verificação de Mensagens de Erro Personalizadas
Você também pode validar mensagens de erro para exceções personalizadas usando o mesmo método.
class CustomError(Exception):
pass
def test_custom_error_message():
def raise_custom_error():
raise CustomError("This is a custom error message.")
with pytest.raises(CustomError, match="custom error message"):
raise_custom_error()
Este teste verifica se um CustomError
é lançado e se sua mensagem contém “custom error message”.
A verificação de mensagens de erro é importante para garantir a consistência das mensagens para os usuários e a precisão das informações de depuração. Use pytest para realizar esses testes de forma eficiente.
Testando Múltiplos Tratamentos de Exceção
Quando uma função pode lançar várias exceções, é importante testar se cada exceção é tratada corretamente. Vamos mostrar como testar múltiplos tratamentos de exceção usando pytest.
Verificando Múltiplas Exceções em um Único Teste
Se uma função lançar diferentes exceções com base em diferentes entradas, você deve testar se cada exceção é tratada adequadamente.
import pytest
def error_prone_function(value):
if value == 0:
raise ValueError("Value cannot be zero")
elif value < 0:
raise TypeError("Value cannot be negative")
return True
def test_multiple_exceptions():
with pytest.raises(ValueError, match="Value cannot be zero"):
error_prone_function(0)
with pytest.raises(TypeError, match="Value cannot be negative"):
error_prone_function(-1)
Neste teste, verificamos se a função error_prone_function
lança um ValueError
quando o valor é 0 e um TypeError
quando o valor é negativo.
Testando Exceções com Testes Parametrizados
Use testes parametrizados para testar de forma eficiente se diferentes entradas geram diferentes exceções.
@pytest.mark.parametrize("value, expected_exception, match_text", [
(0, ValueError, "Value cannot be zero"),
(-1, TypeError, "Value cannot be negative")
])
def test_error_prone_function(value, expected_exception, match_text):
with pytest.raises(expected_exception, match=match_text):
error_prone_function(value)
Neste teste parametrizado, verificamos se o value
gera a exceção esperada com a mensagem de erro correspondente.
Testando Múltiplas Exceções com um Método Personalizado
Também é possível testar uma função que lança várias exceções utilizando um método personalizado para simplificar os testes.
def test_custom_multiple_exceptions():
def assert_raises_with_message(func, exception, match_text):
with pytest.raises(exception, match=match_text):
func()
assert_raises_with_message(lambda: error_prone_function(0), ValueError, "Value cannot be zero")
assert_raises_with_message(lambda: error_prone_function(-1), TypeError, "Value cannot be negative")
Nesse exemplo, utilizamos o método personalizado assert_raises_with_message
para verificar se a função lança a exceção correta com a mensagem de erro esperada.
Verificar múltiplas exceções em um único teste ajuda a reduzir a duplicação de código e melhora a manutenção dos testes. Aproveite os recursos do pytest para realizar testes de exceções de forma eficiente.
Testando Exceções Personalizadas
Definir e usar exceções personalizadas pode tornar o tratamento de erros mais claro e permitir respostas adequadas para situações de erro específicas. Vamos explicar como testar exceções personalizadas.
Definindo Exceções Personalizadas
Primeiro, defina uma exceção personalizada. Crie uma nova classe de exceção herdando de uma exceção padrão do Python.
class CustomError(Exception):
"""Classe base para exceções personalizadas"""
pass
class SpecificError(CustomError):
"""Exceção personalizada para erros específicos"""
pass
Função que Lança uma Exceção Personalizada
Em seguida, crie uma função que lança a exceção personalizada sob certas condições.
def function_that_raises(value):
if value == 'error':
raise SpecificError("An error occurred with value: error")
return True
Testando Exceções Personalizadas
Use o pytest para verificar se a exceção personalizada é lançada corretamente.
import pytest
def test_specific_error():
with pytest.raises(SpecificError, match="An error occurred with value: error"):
function_that_raises('error')
Neste teste, verificamos se a função function_that_raises
lança um SpecificError
com a mensagem esperada.
Testando Múltiplas Exceções Personalizadas
Se você usar múltiplas exceções personalizadas, também pode testar se cada exceção é tratada corretamente.
class AnotherCustomError(Exception):
"""Outra exceção personalizada"""
pass
def function_with_multiple_custom_errors(value):
if value == 'first':
raise SpecificError("First error occurred")
elif value == 'second':
raise AnotherCustomError("Second error occurred")
return True
def test_multiple_custom_errors():
with pytest.raises(SpecificError, match="First error occurred"):
function_with_multiple_custom_errors('first')
with pytest.raises(AnotherCustomError, match="Second error occurred"):
function_with_multiple_custom_errors('second')
Neste teste, verificamos se a função function_with_multiple_custom_errors
lança a exceção personalizada correta para diferentes entradas.
Verificando Mensagens de Erro em Exceções Personalizadas
Também é importante validar se as mensagens de erro das exceções personalizadas são precisas.
def test_custom_error_message():
with pytest.raises(SpecificError, match="An error occurred with value: error"):
function_that_raises('error')
Neste teste, verificamos se a mensagem de erro do SpecificError
contém “An error occurred with value: error”.
Testar exceções personalizadas garante que a aplicação lide adequadamente com situações de erro específicas, melhorando a qualidade e a confiabilidade do código.
Exemplo Prático: Testando Erros em APIs
No desenvolvimento de APIs, o tratamento de erros é essencial. Para garantir que o cliente receba mensagens de erro adequadas, é necessário realizar testes de erros. Vamos mostrar como testar o tratamento de erros em APIs com pytest.
Exemplo de API com FastAPI
Primeiro, definimos um endpoint simples com FastAPI, que gera erros específicos.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 0:
raise HTTPException(status_code=400, detail="Item ID cannot be zero")
if item_id < 0:
raise HTTPException(status_code=404, detail="Item not found")
return {"item_id": item_id, "name": "Item Name"}
Este endpoint retorna um erro 400 se o item_id for 0 e um erro 404 se o item_id for negativo.
Testando Erros com pytest e httpx
Agora, usaremos pytest e httpx para testar os erros da API.
import pytest
from httpx import AsyncClient
from main import app
@pytest.mark.asyncio
async def test_read_item_zero():
async with AsyncClient(app=app, base_url="http://test") as ac:
response = await ac.get("/items/0")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID cannot be zero"}
@pytest.mark.asyncio
async def test_read_item_negative():
async with AsyncClient(app=app, base_url="http://test") as ac:
response = await ac.get("/items/-1")
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
Esses testes verificam se a solicitação para /items/0
retorna um erro 400 com a mensagem de erro esperada e se a solicitação para /items/-1
retorna um erro 404 com a mensagem de erro correta.
Automatizando e Otimizando Testes de Erro
Use testes parametrizados para agrupar diferentes casos de erro em um único teste.
@pytest.mark.asyncio
@pytest.mark.parametrize("item_id, expected_status, expected_detail", [
(0, 400, "Item ID cannot be zero"),
(-1, 404, "Item not found"),
])
async def test_read_item_errors(item_id, expected_status, expected_detail):
async with AsyncClient(app=app, base_url="http://test") as ac:
response = await ac.get(f"/items/{item_id}")
assert response.status_code == expected_status
assert response.json() == {"detail": expected_detail}
Nesse teste parametrizado, verificamos se diferentes item_id
retornam os códigos de status e mensagens de erro esperados, mantendo o código do teste conciso e eficiente.
Realizar testes de erros em APIs garante que o cliente receba as mensagens de erro corretas, melhorando a confiabilidade da aplicação.
Aproveitando Fixtures do pytest para Testes de Erro
As fixtures são um recurso poderoso do pytest para gerenciar a configuração e limpeza dos testes. Utilizar fixtures para testes de erro pode melhorar a reutilização e a legibilidade do código.
Conceitos Básicos de Fixtures
Primeiro, vamos explicar o uso básico das fixtures no pytest. As fixtures permitem definir tarefas de preparação comuns que podem ser reutilizadas em vários testes.
import pytest
@pytest.fixture
def sample_data():
return {"name": "test", "value": 42}
def test_sample_data(sample_data):
assert sample_data["name"] == "test"
assert sample_data["value"] == 42
Neste exemplo, a fixture sample_data
é usada para fornecer dados de teste ao teste.
Usando Fixtures para Configurar Testes de API
Para testes de API, podemos usar fixtures para configurar o cliente. No exemplo abaixo, configuramos o AsyncClient
do httpx como uma fixture.
import pytest
from httpx import AsyncClient
from main import app
@pytest.fixture
async def async_client():
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
@pytest.mark.asyncio
async def test_read_item_zero(async_client):
response = await async_client.get("/items/0")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID cannot be zero"}
Nesse teste, usamos a fixture async_client
para configurar o cliente e enviar a solicitação para a API.
Aproveitando Fixtures para Vários Testes de Erro
O uso de fixtures pode simplificar a execução de vários testes de erro.
@pytest.mark.asyncio
@pytest.mark.parametrize("item_id, expected_status, expected_detail", [
(0, 400, "Item ID cannot be zero"),
(-1, 404, "Item not found"),
])
async def test_read_item_errors(async_client, item_id, expected_status, expected_detail):
response = await async_client.get(f"/items/{item_id}")
assert response.status_code == expected_status
assert response.json() == {"detail": expected_detail}
Neste exemplo, combinamos a fixture async_client
com testes parametrizados para testar diferentes casos de erro de forma concisa.
Fixture para Conexão com Banco de Dados
Se os testes envolverem conexão com um banco de dados, podemos usar fixtures para gerenciar a configuração e limpeza da conexão.
import sqlalchemy
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "sqlite+aiosqlite:///./test.db"
@pytest.fixture
async def async_db_session():
engine = create_async_engine(DATABASE_URL, echo=True)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
yield session
await engine.dispose()
async def test_db_interaction(async_db_session):
result = await async_db_session.execute("SELECT 1")
assert result.scalar() == 1
Nesse exemplo, usamos a fixture async_db_session
para gerenciar a conexão com o banco de dados e garantir a limpeza após o teste.
O uso de fixtures reduz a duplicação de código e facilita a manutenção dos testes. Aproveite as fixtures do pytest para realizar testes de erro de forma eficiente.
Conclusão
Testar exceções e erros com pytest é essencial para melhorar a qualidade do software. Neste artigo, abordamos desde a configuração básica, passando pela forma de testar exceções, verificação de mensagens de erro, testes de múltiplos tratamentos de exceção, testes de exceções personalizadas, testes de erro em APIs e o uso de fixtures para testes de erro.
Aproveitando ao máximo os recursos do pytest, os testes de tratamento de erros podem ser realizados de forma eficiente e eficaz, aumentando a confiabilidade e a manutenção do código, e evitando problemas causados por erros inesperados. Continue aprendendo sobre as técnicas de teste com pytest e busque um desenvolvimento de software de alta qualidade.