Ein vollständiger Leitfaden für Ausnahme- und Fehlerprüfung mit pytest in Python

In der Softwareentwicklung sind Tests zur Behandlung von Ausnahmen und Fehlern von entscheidender Bedeutung. Durch geeignete Tests kann die Zuverlässigkeit des Codes erhöht und das Auftreten unerwarteter Fehler vermieden werden. In diesem Artikel wird erläutert, wie Sie mit dem Python-Testframework pytest Ausnahmen und Fehlerbehandlungen effizient testen können. Wir erklären schrittweise alles, von der grundlegenden Einrichtung bis hin zur Prüfung benutzerdefinierter Ausnahmen und mehrerer Fehlerbehandlungen.

Inhaltsverzeichnis

Grundlegende Konfiguration von pytest

pytest ist ein leistungsstarkes Testframework für Python, das sich einfach installieren und verwenden lässt. Befolgen Sie die folgenden Schritte, um pytest einzurichten.

Installation von pytest

Zuerst müssen Sie pytest installieren. Verwenden Sie dazu den folgenden Befehl, um die Installation über pip durchzuführen.

pip install pytest

Grundlegende Verzeichnisstruktur

Für die Verzeichnisstruktur des Projekts empfiehlt es sich, die folgende Struktur zu verwenden.

my_project/
├── src/
│   └── my_module.py
└── tests/
    ├── __init__.py
    └── test_my_module.py

Erstellen der ersten Testdatei

Als Nächstes erstellen Sie eine Python-Datei für die Tests. Erstellen Sie beispielsweise eine Datei namens test_my_module.py im Verzeichnis tests und fügen Sie den folgenden Inhalt hinzu.

def test_example():
    assert 1 + 1 == 2

Ausführen von Tests

Um Tests auszuführen, führen Sie den folgenden Befehl im Hauptverzeichnis des Projekts aus.

pytest

Dadurch erkennt pytest automatisch die Testdateien im Verzeichnis tests und führt die Tests aus. Wenn die Tests erfolgreich sind, ist die Grundeinrichtung abgeschlossen.

Methoden zur Prüfung von Ausnahmen

Um sicherzustellen, dass Ausnahmen korrekt ausgelöst werden, bietet pytest eine sehr nützliche Methode. Hier wird erklärt, wie man prüft, ob eine bestimmte Ausnahme ausgelöst wird.

Ausnahmeprüfung mit pytest.raises

Um sicherzustellen, dass eine Ausnahme ausgelöst wird, verwenden Sie den Kontextmanager pytest.raises. Im folgenden Beispiel wird getestet, ob eine Division durch Null auftritt.

import pytest

def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0

In diesem Test wird überprüft, ob bei der Ausführung von 1 / 0 eine ZeroDivisionError ausgelöst wird.

Überprüfung der Ausnahmemeldung

Manchmal ist es nicht nur wichtig, dass eine Ausnahme auftritt, sondern auch, dass eine bestimmte Fehlermeldung enthalten ist. In diesem Fall verwenden Sie den Parameter match.

def test_zero_division_message():
    with pytest.raises(ZeroDivisionError, match="division by zero"):
        1 / 0

In diesem Test wird überprüft, ob ein ZeroDivisionError auftritt und die Fehlermeldung „division by zero“ enthält.

Testen mehrerer Ausnahmen

Es ist auch möglich, mehrere Ausnahmen in einem einzigen Testfall zu testen, z. B. um zu überprüfen, ob unter verschiedenen Bedingungen unterschiedliche Ausnahmen auftreten.

def test_multiple_exceptions():
    with pytest.raises(ZeroDivisionError):
        1 / 0

    with pytest.raises(TypeError):
        '1' + 1

In diesem Test wird zuerst überprüft, ob ein ZeroDivisionError auftritt, und dann, ob ein TypeError auftritt.

Durch das ordnungsgemäße Testen von Ausnahmen kann sichergestellt werden, dass der Code ordnungsgemäß mit Fehlern umgeht, ohne unerwartetes Verhalten zu zeigen.

Überprüfung von Fehlermeldungen

Es ist wichtig, in Tests zu überprüfen, ob eine bestimmte Fehlermeldung enthalten ist. In diesem Abschnitt wird erläutert, wie Sie mit pytest Fehlermeldungen überprüfen können.

Überprüfung einer bestimmten Fehlermeldung

Es ist möglich zu testen, ob eine Ausnahme nicht nur auftritt, sondern auch eine bestimmte Fehlermeldung enthält. Verwenden Sie pytest.raises und geben Sie die Fehlermeldung über den Parameter match an.

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()

In diesem Test wird überprüft, ob ein ValueError auftritt und die Nachricht „specific message“ enthält.

Überprüfung der Fehlermeldung mit regulären Ausdrücken

Wenn Fehlermeldungen dynamisch generiert werden oder eine Teilübereinstimmung überprüft werden soll, sind reguläre Ausdrücke hilfreich.

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()

In diesem Test wird überprüft, ob die Nachricht des TypeError den Ausdruck „invalid type“ enthält. Indem Sie einen regulären Ausdruck an den Parameter match übergeben, können Sie eine Teilübereinstimmung überprüfen.

Überprüfung benutzerdefinierter Fehlermeldungen

Benutzerdefinierte Ausnahmen können auf dieselbe Weise wie Standardausnahmen überprüft werden.

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()

In diesem Test wird überprüft, ob ein CustomError auftritt und die Nachricht „custom error message“ enthält.

Die Überprüfung von Fehlermeldungen ist wichtig, um die Konsistenz der Fehlermeldungen für Benutzer und die Genauigkeit von Debug-Informationen sicherzustellen. Nutzen Sie pytest, um diese Tests effizient durchzuführen.

Testen mehrerer Ausnahmebehandlungen

Wenn eine Funktion mehrere Ausnahmen auslösen kann, ist es wichtig zu testen, ob jede Ausnahme ordnungsgemäß behandelt wird. In diesem Abschnitt wird erklärt, wie Sie mehrere Ausnahmebehandlungen mit pytest testen können.

Überprüfung mehrerer Ausnahmen in einem Test

Wenn eine Funktion unter verschiedenen Eingabewerten unterschiedliche Ausnahmen auslöst, sollten Sie testen, ob jede Ausnahme ordnungsgemäß behandelt wird.

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)

In diesem Test wird überprüft, ob error_prone_function bei einem Wert von 0 eine ValueError und bei einem negativen Wert eine TypeError auslöst.

Verwendung von parametrisierten Tests zur Überprüfung von Ausnahmen

Verwenden Sie parametrisierte Tests, um effizient zu testen, dass eine Funktion bei unterschiedlichen Eingabewerten unterschiedliche Ausnahmen auslöst.

@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)

In diesem parametrisierten Test wird die Funktion mit unterschiedlichen Werten getestet, wobei die erwarteten Ausnahmen und Fehlermeldungen kombiniert werden.

Testen mehrerer Ausnahmen mit benutzerdefinierten Methoden

Eine Funktion, die mehrere Ausnahmen auslöst, kann auch effizient mit benutzerdefinierten Methoden getestet werden.

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")

In diesem Test wird die benutzerdefinierte Methode assert_raises_with_message verwendet, um zu überprüfen, ob die Funktion bei bestimmten Eingaben die richtigen Ausnahmen und Fehlermeldungen auslöst.

Durch die Überprüfung mehrerer Ausnahmen in einem Testfall kann der Testcode vereinfacht und die Wartbarkeit erhöht werden. Nutzen Sie die Funktionen von pytest, um effiziente Tests für Ausnahmebehandlungen durchzuführen.

Testen von benutzerdefinierten Ausnahmen

Durch die Definition und Verwendung benutzerdefinierter Ausnahmen können die Fehlerbehandlung einer Anwendung klarer gestaltet und spezifische Fehlerzustände besser behandelt werden. In diesem Abschnitt wird erläutert, wie benutzerdefinierte Ausnahmen getestet werden können.

Definition einer benutzerdefinierten Ausnahme

Zuerst definieren Sie eine benutzerdefinierte Ausnahme. Erstellen Sie eine neue Ausnahmeklasse, indem Sie die eingebaute Ausnahmeklasse von Python erweitern.

class CustomError(Exception):
    """Die Basisklasse für benutzerdefinierte Ausnahmen"""
    pass

class SpecificError(CustomError):
    """Eine benutzerdefinierte Ausnahme für spezifische Fehler"""
    pass

Eine Funktion, die benutzerdefinierte Ausnahmen auslöst

Erstellen Sie als Nächstes eine Funktion, die unter bestimmten Bedingungen eine benutzerdefinierte Ausnahme auslöst.

def function_that_raises(value):
    if value == 'error':
        raise SpecificError("An error occurred with value: error")
    return True

Testen von benutzerdefinierten Ausnahmen

Verwenden Sie pytest, um zu überprüfen, ob benutzerdefinierte Ausnahmen korrekt ausgelöst werden.

import pytest

def test_specific_error():
    with pytest.raises(SpecificError, match="An error occurred with value: error"):
        function_that_raises('error')

In diesem Test wird überprüft, ob die Funktion function_that_raises eine SpecificError auslöst und die Fehlermeldung wie erwartet ist.

Testen mehrerer benutzerdefinierter Ausnahmen

Wenn mehrere benutzerdefinierte Ausnahmen verwendet werden, sollten Sie testen, ob jede Ausnahme ordnungsgemäß behandelt wird.

class AnotherCustomError(Exception):
    """Eine weitere benutzerdefinierte Ausnahme"""
    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')

In diesem Test wird überprüft, ob die Funktion function_with_multiple_custom_errors bei verschiedenen Eingabewerten die richtigen benutzerdefinierten Ausnahmen auslöst.

Überprüfung der Nachricht benutzerdefinierter Ausnahmen

Es ist auch wichtig zu überprüfen, ob die Fehlermeldung einer benutzerdefinierten Ausnahme korrekt ist.

def test_custom_error_message():
    with pytest.raises(SpecificError, match="An error occurred with value: error"):
        function_that_raises('error')

In diesem Test wird überprüft, ob ein SpecificError auftritt und die Fehlermeldung „An error occurred with value: error“ enthält.

Durch das Testen benutzerdefinierter Ausnahmen kann sichergestellt werden, dass die Anwendung Fehlerzustände korrekt handhabt und die Qualität und Zuverlässigkeit des Codes verbessert wird.

Anwendungsbeispiel: Fehlerprüfung bei APIs

Bei der Entwicklung von APIs ist die Fehlerbehandlung wichtig. Damit Clients geeignete Fehlermeldungen erhalten, müssen Fehlerprüfungen durchgeführt werden. In diesem Abschnitt wird erklärt, wie Sie die Fehlerbehandlung einer API mit pytest testen können.

Beispiel-API mit FastAPI

Zunächst definieren wir einen einfachen FastAPI-Endpunkt, der spezifische Fehler auslöst.

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"}

Dieser Endpunkt gibt einen Fehler 400 zurück, wenn item_id gleich 0 ist, und einen Fehler 404, wenn item_id negativ ist.

Fehlerprüfung mit pytest und httpx

Verwenden Sie als Nächstes pytest und httpx, um die Fehlerprüfung der API durchzuführen.

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"}

In diesen Tests wird überprüft, ob die Anfragen an /items/0 und /items/-1 die erwarteten Fehlercodes und Fehlermeldungen zurückgeben.

Automatisierung und Effizienzsteigerung bei Fehlerprüfungen

Durch die Verwendung von parametrisierten Tests können verschiedene Fehlerfälle in einem einzigen Test zusammengefasst werden.

@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}

In diesem parametrisierten Test werden verschiedene item_id-Werte getestet, um sicherzustellen, dass die erwarteten Statuscodes und Fehlermeldungen zurückgegeben werden.

Durch die Fehlerprüfung von APIs kann sichergestellt werden, dass Clients die richtigen Fehlermeldungen erhalten, was die Zuverlässigkeit der Anwendung erhöht.

Verwendung von pytest-Fixtures bei Fehlerprüfungen

Fixtures sind eine leistungsstarke Funktion von pytest, die das Setup und das Aufräumen von Tests effizient verwaltet. Durch die Verwendung von Fixtures bei Fehlerprüfungen kann die Wiederverwendbarkeit und Lesbarkeit des Codes verbessert werden.

Grundlagen von Fixtures

Zuerst werden die Grundlagen von pytest-Fixtures erläutert. Fixtures ermöglichen die Definition gemeinsamer Setup-Aufgaben, die in mehreren Tests verwendet werden können.

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

In diesem Beispiel wird ein Fixture sample_data definiert und in der Testfunktion verwendet.

Verwendung von Fixtures zum Einrichten von APIs

Bei API-Tests können Fixtures verwendet werden, um die Einrichtung des Clients vorzunehmen. Im folgenden Beispiel wird AsyncClient von httpx als Fixture konfiguriert.

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"}

In diesem Test wird das Fixture async_client verwendet, um die Einrichtung des Clients vorzunehmen und eine API-Anfrage zu senden.

Effiziente Fehlerprüfungen mit Fixtures

Durch die Kombination von Fixtures und parametrisierten Tests können mehrere Fehlerprüfungen effizient durchgeführt werden.

@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}

In diesem Beispiel werden das Fixture async_client und parametrisierte Tests kombiniert, um verschiedene Fehlerfälle kompakt zu beschreiben.

Fixture für Datenbankverbindungen

Wenn Tests eine Datenbankverbindung erfordern, können Fixtures verwendet werden, um die Verbindung einzurichten und aufzuräumen.

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

In diesem Beispiel wird das Fixture async_db_session verwendet, um die Verwaltung der Datenbankverbindung durchzuführen und nach dem Test aufzuräumen.

Durch den Einsatz von Fixtures kann der Testcode vereinfacht und die Wartung erleichtert werden. Nutzen Sie pytest-Fixtures, um effiziente Fehlerprüfungen durchzuführen.

Zusammenfassung

Die Verwendung von pytest für Ausnahme- und Fehlerprüfungen ist unerlässlich, um die Qualität der Software zu verbessern. In diesem Artikel haben wir alles von den grundlegenden Einstellungen über Methoden zur Prüfung von Ausnahmen, Überprüfung von Fehlermeldungen, Testen mehrerer Ausnahmebehandlungen, benutzerdefinierte Ausnahmen, Fehlerprüfungen bei APIs bis hin zur Nutzung von Fixtures für effiziente Tests behandelt.

Durch den umfassenden Einsatz der pytest-Funktionen können Fehlerprüfungen effizient und effektiv durchgeführt werden. Dies führt zu einer höheren Zuverlässigkeit und Wartbarkeit des Codes und verhindert Probleme, die durch unerwartete Fehler verursacht werden. Lernen Sie weiterhin Testmethoden mit pytest, um eine qualitativ hochwertige Softwareentwicklung zu gewährleisten.

Inhaltsverzeichnis