Dans le développement logiciel, tester la gestion des exceptions et des erreurs est crucial. En effectuant des tests appropriés, vous pouvez améliorer la fiabilité de votre code et prévenir les défaillances dues à des erreurs inattendues. Cet article explique en détail comment tester efficacement les exceptions et la gestion des erreurs en utilisant pytest, le framework de test pour Python. Nous aborderons les étapes, de la configuration de base aux tests des exceptions personnalisées et la gestion de multiples exceptions.
Configuration de base de pytest
pytest est un framework de test puissant pour Python, facile à installer et à utiliser. Suivez les étapes ci-dessous pour configurer pytest.
Installation de pytest
Tout d’abord, vous devez installer pytest. Utilisez la commande suivante pour l’installer via pip.
pip install pytest
Structure de répertoire de base
Il est recommandé d’organiser le répertoire de tests du projet comme suit :
my_project/
├── src/
│ └── my_module.py
└── tests/
├── __init__.py
└── test_my_module.py
Création du premier fichier de test
Ensuite, créez un fichier Python pour les tests. Par exemple, créez un fichier nommé test_my_module.py
dans le répertoire tests
et ajoutez le contenu suivant.
def test_example():
assert 1 + 1 == 2
Exécution des tests
Pour exécuter les tests, lancez la commande suivante dans le répertoire racine du projet.
pytest
Cela permettra à pytest de détecter automatiquement les fichiers de test dans le répertoire tests
et de les exécuter. Si les tests réussissent, la configuration de base est terminée.
Méthode de test des exceptions
pytest propose des moyens très pratiques pour vérifier si des exceptions sont correctement levées. Voici comment tester qu’une exception spécifique est levée.
Tester les exceptions avec pytest.raises
Pour vérifier qu’une exception est levée, utilisez le gestionnaire de contexte pytest.raises
. L’exemple suivant teste la levée d’une exception de division par zéro.
import pytest
def test_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0
Ce test vérifie que l’opération 1 / 0
génère une exception ZeroDivisionError
.
Vérification des messages d’exception
Il est parfois nécessaire de vérifier que l’exception levée contient un message d’erreur spécifique. Utilisez le paramètre match
pour cela.
def test_zero_division_message():
with pytest.raises(ZeroDivisionError, match="division by zero"):
1 / 0
Ce test vérifie que l’exception ZeroDivisionError
est levée et que le message d’erreur contient « division by zero ».
Tester plusieurs exceptions
Il est possible de tester plusieurs exceptions dans un seul cas de test, par exemple pour vérifier que différentes exceptions se produisent dans des conditions différentes.
def test_multiple_exceptions():
with pytest.raises(ZeroDivisionError):
1 / 0
with pytest.raises(TypeError):
'1' + 1
Ce test vérifie d’abord que ZeroDivisionError
est levée, puis que TypeError
se produit.
En effectuant correctement les tests des exceptions, vous pouvez garantir que votre code gère les erreurs de manière appropriée sans comportement inattendu.
Validation des messages d’erreur
Il est essentiel de vérifier que certains messages d’erreur sont présents dans les tests. Voici comment valider les messages d’erreur avec pytest.
Vérification d’un message d’erreur spécifique
Vous pouvez tester non seulement qu’une exception est levée, mais aussi que le message de cette exception correspond à un texte précis, en utilisant le paramètre match
de pytest.raises
.
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()
Ce test vérifie que l’exception ValueError
est levée et que son message contient « specific message ».
Validation des messages d’erreur avec des expressions régulières
Lorsque les messages d’erreur sont générés dynamiquement ou pour vérifier une correspondance partielle, les expressions régulières peuvent être utiles.
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()
Ce test vérifie que le message de l’exception TypeError
contient l’expression « invalid type ». En passant une expression régulière au paramètre match
, il est possible de valider une correspondance partielle.
Validation des messages d’erreur personnalisés
Les exceptions personnalisées peuvent également être validées de la même manière pour vérifier leurs messages d’erreur.
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()
Ce test vérifie que l’exception CustomError
est levée et que son message contient « custom error message ».
La validation des messages d’erreur est importante pour garantir la cohérence des messages d’erreur fournis aux utilisateurs et la précision des informations de débogage. Utilisez pytest pour réaliser ces tests de manière efficace.
Test de la gestion de multiples exceptions
Lorsqu’une fonction peut lever plusieurs exceptions, il est important de tester que chacune est gérée correctement. Voici comment utiliser pytest pour tester différentes gestions d’exceptions de manière groupée.
Vérification de plusieurs exceptions dans un seul test
Si une fonction peut lever différentes exceptions selon les conditions, il est possible de tester la gestion de chacune d’elles dans le même cas de test.
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)
Ce test vérifie que la fonction error_prone_function
lève une ValueError
si la valeur est 0 et une TypeError
si elle est négative.
Tests d’exceptions paramétrés
Pour tester efficacement qu’une fonction lève différentes exceptions, vous pouvez utiliser des tests paramétrés.
@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)
Ce test paramétré permet de tester différentes valeurs pour le paramètre value
et de vérifier que les exceptions et les messages correspondants sont corrects.
Test des exceptions avec une méthode personnalisée
Vous pouvez également créer une méthode personnalisée pour tester efficacement les exceptions levées par une fonction.
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")
Ce test utilise la méthode assert_raises_with_message
pour vérifier que la fonction lève les bonnes exceptions avec les bons messages d’erreur.
Tester plusieurs exceptions dans un seul test permet de réduire la duplication de code et d’améliorer la maintenabilité des tests. Utilisez les fonctionnalités de pytest pour réaliser efficacement les tests de gestion des exceptions.
Test des exceptions personnalisées
Définir et utiliser des exceptions personnalisées permet de clarifier la gestion des erreurs dans l’application et de mieux traiter les situations d’erreur spécifiques. Voici comment tester les exceptions personnalisées.
Définition des exceptions personnalisées
Commencez par définir une exception personnalisée. Héritez de la classe d’exception intégrée de Python pour créer une nouvelle classe d’exception.
class CustomError(Exception):
"""Classe de base pour les exceptions personnalisées"""
pass
class SpecificError(CustomError):
"""Exception personnalisée pour des erreurs spécifiques"""
pass
Fonction levant une exception personnalisée
Ensuite, créez une fonction qui lève l’exception personnalisée dans certaines conditions.
def function_that_raises(value):
if value == 'error':
raise SpecificError("An error occurred with value: error")
return True
Test des exceptions personnalisées
Utilisez pytest pour tester que les exceptions personnalisées sont correctement levées.
import pytest
def test_specific_error():
with pytest.raises(SpecificError, match="An error occurred with value: error"):
function_that_raises('error')
Ce test vérifie que la fonction function_that_raises
lève une SpecificError
avec le message attendu.
Test de plusieurs exceptions personnalisées
Lors de l’utilisation de plusieurs exceptions personnalisées, testez que chacune est correctement gérée.
class AnotherCustomError(Exception):
"""Une autre exception personnalisée"""
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')
Ce test vérifie que la fonction function_with_multiple_custom_errors
lève les bonnes exceptions personnalisées pour les valeurs d’entrée fournies.
Vérification des messages des exceptions personnalisées
Il est également important de vérifier que les messages d’erreur des exceptions personnalisées sont corrects.
def test_custom_error_message():
with pytest.raises(SpecificError, match="An error occurred with value: error"):
function_that_raises('error')
Ce test vérifie que l’exception SpecificError
contient le message « An error occurred with value: error ».
Les tests des exceptions personnalisées permettent de garantir que l’application peut gérer les situations d’erreur spécifiques de manière adéquate, ce qui améliore la qualité et la fiabilité du code.
Exemple avancé : Test des erreurs d’une API
Lors du développement d’une API, la gestion des erreurs est essentielle. Pour que le client reçoive des messages d’erreur appropriés, il est nécessaire de tester la gestion des erreurs. Voici comment tester la gestion des erreurs d’une API avec pytest.
Exemple d’API avec FastAPI
Tout d’abord, définissons un endpoint FastAPI simple. Cet exemple crée un endpoint qui génère des erreurs spécifiques.
from fastapi import FastAPI, HTTPExceptionapp = 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"}
Ce endpoint renvoie une erreur 400 si item_id
est 0, et une erreur 404 si item_id
est négatif.
Test des erreurs d’une API avec pytest et httpx
Utilisons pytest et httpx pour tester les erreurs de l’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"}
Ces tests envoient des requêtes à /items/0
et /items/-1
et vérifient respectivement les erreurs 400 et 404 avec les messages d’erreur appropriés.
Automatisation et optimisation des tests d’erreurs
Utilisez les tests paramétrés pour regrouper les différents cas de test d’erreurs.
@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}
Ce test paramétré vérifie les différents item_id
et les codes de statut attendus ainsi que les messages d’erreur, rendant les tests plus concis et faciles à maintenir.
Tester les erreurs d’une API permet de s’assurer que les clients reçoivent des messages d’erreur appropriés, améliorant ainsi la fiabilité de l’application.
Utilisation des fixtures pytest pour les tests d’erreurs
Les fixtures sont une fonctionnalité puissante de pytest pour gérer les configurations et les nettoyages de tests. Elles permettent d’améliorer la réutilisabilité et la lisibilité des tests d’erreurs.
Bases des fixtures
Commençons par les bases des fixtures pytest. Elles permettent de regrouper des tâches de configuration communes et de les réutiliser dans plusieurs tests.
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
Dans cet exemple, la fixture sample_data
est définie et utilisée dans la fonction de test.
Utilisation d’une fixture pour la configuration d’une API
Dans les tests d’API, une fixture peut être utilisée pour configurer le client de test. L’exemple suivant montre comment configurer un AsyncClient
httpx en tant que 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"}
Dans ce test, la fixture async_client
est utilisée pour configurer le client et envoyer les requêtes API.
Utilisation des fixtures pour tester plusieurs erreurs
Les fixtures permettent de rendre les tests d’erreurs plus efficaces en les combinant avec les tests paramétrés.
@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}
Dans cet exemple, la combinaison de la fixture async_client
et du test paramétré permet d’écrire les cas de test d’erreurs de manière concise.
Fixture pour la connexion à une base de données
Pour les tests nécessitant une connexion à une base de données, une fixture peut gérer la configuration et le nettoyage de la connexion.
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
Dans cet exemple, la fixture async_db_session
gère la connexion à la base de données et effectue le nettoyage après les tests.
L’utilisation des fixtures permet de réduire la duplication du code de test et facilite la maintenance des tests. Exploitez les fixtures pytest pour réaliser des tests d’erreurs efficaces.
Conclusion
Les tests d’exceptions et d’erreurs avec pytest sont essentiels pour améliorer la qualité des logiciels. Cet article a couvert la configuration de base, les méthodes de test des exceptions, la validation des messages d’erreur, les tests de gestion de multiples exceptions, les tests d’exceptions personnalisées, les tests d’erreurs d’API, et l’utilisation des fixtures pour les tests d’erreurs.
En exploitant pleinement les fonctionnalités de pytest, vous pouvez effectuer des tests de gestion des erreurs de manière efficace et efficiente. Cela permet d’améliorer la fiabilité et la maintenabilité du code, tout en évitant les problèmes dus à des erreurs inattendues. Continuez à apprendre les techniques de test avec pytest pour développer des logiciels de meilleure qualité.