25. März 20264 min

Pytest in der Praxis 2026: Testing Guide fuer Python Entwickler

Ein praktischer 2026 Pytest Guide fuer Python Entwickler mit ersten Tests, Parametrisierung, Marks, Fixtures, pytest.raises, Mocks, Monkeypatching und Teardown Patterns.

Testing ist 2026 ein Kernbestandteil moderner Softwareentwicklung. Manche Entwickler schreiben Tests nach der Implementierung. Andere bevorzugen TDD, bei dem der Test zuerst kommt und der Production Code danach folgt.

In beiden Faellen hilft eine gesunde Test-Suite, Bugs frueh zu finden, Refactoring abzusichern und Projekte stabil zu halten, wenn sie wachsen.

Fuer Python ist pytest eines der besten Alltagswerkzeuge: maechtig, flexibel und angenehm genug fuer kleine Skripte genauso wie fuer grosse Anwendungen.

Dieser Guide behandelt die wichtigsten Bausteine: erste Tests, Parametrisierung, Marks, Fixtures, Exception Assertions, Mocks, Monkeypatching und Teardown Patterns.

Warum automatisierte Tests wichtig sind

Automatisierte Tests garantieren nicht, dass deine Software fehlerfrei ist. Sie reduzieren aber die Chance, offensichtliche Regressionen und schwere Logikfehler auszuliefern.

Gute Tests funktionieren ausserdem als lebendige Dokumentation. Sie zeigen, wie sich Code in echten Szenarien verhalten soll.

Am wichtigsten: Tests machen Refactoring weniger riskant. Wenn die Suite verlaesslich ist, kannst du Internals aendern und das Verhalten trotzdem schuetzen.

Dein erster Pytest Test

Installiere Pytest:

pip install pytest

Erzeuge test_example.py:

def test_answer() -> None:
    assert 2 + 2 == 4

Fuehre die Tests aus:

pytest .

Du solltest einen erfolgreichen Test sehen:

============ test session starts ============
collected 1 item

test_example.py .                            [100%]

============ 1 passed in 0.00s =============

Wenn du die Assertion zu assert 2 + 2 == 5 aenderst, zeigt Pytest eine klare Fehlermeldung:

E       assert 4 == 5

Diese gut lesbare Assertion-Ausgabe ist ein Grund, warum Pytest so produktiv wirkt.

Ein nuetzlicheres Beispiel: Factorial

Hier ist eine kleine Factorial-Funktion:

def factorial(n: int) -> int:
    if n in [0, 1]:
        return 1
    return n * factorial(n - 1)

Ein einzelner Test funktioniert:

def test_factorial() -> None:
    expected = 120
    got = factorial(5)
    assert expected == got

Aber 0, 1 und 5 sind unterschiedliche Faelle. Separate Tests sind okay:

def test_factorial_return_one_if_zero() -> None:
    assert factorial(0) == 1


def test_factorial_return_one_if_one() -> None:
    assert factorial(1) == 1


def test_factorial_of_five() -> None:
    assert factorial(5) == 120

Die Tests sind fast identisch, daher ist Parametrisierung sauberer.

import pytest


@pytest.mark.parametrize(
    ("number", "expected"),
    [
        (0, 1),
        (1, 1),
        (5, 120),
    ],
)
def test_factorial(number: int, expected: int) -> None:
    assert factorial(number) == expected

Diese eine Testfunktion laeuft einmal pro Input-Paar.

Nuetzliche Pytest Marks

@pytest.mark.parametrize buendelt mehrere Szenarien in einem Test. Du kannst mehrere Parametrisierung-Decorator stacken, aber Vorsicht: Kombinationen wachsen schnell.

@pytest.mark.skip ueberspringt einen Test komplett:

@pytest.mark.skip(reason="Awaiting fix in the next library release")
def test_some_function() -> None:
    ...

@pytest.mark.skipif ueberspringt einen Test nur, wenn eine Bedingung wahr ist:

import sys
import pytest


@pytest.mark.skipif(sys.platform == "win32", reason="Unix-specific test")
def test_unix_behavior() -> None:
    ...

@pytest.mark.xfail markiert einen bekannten Fehler, ohne den kompletten Build zu brechen:

@pytest.mark.xfail(reason="Waiting for new data updates on the server")
def test_data_sync() -> None:
    ...

Nutze Marks, um Test-Intent sichtbar zu machen, statt fragiles Verhalten zu verstecken.

Fixtures fuer Setup und Teardown

Angenommen, eine Klasse liest Preise aus verschiedenen Datenquellen:

class PriceManager:
    def __init__(self, x_price_source, y_price_source):
        self.x_price_source = x_price_source
        self.y_price_source = y_price_source

    def get_price(self, product):
        if product.type == "x":
            return self.x_price_source.get(product)
        if product.type == "y":
            return self.y_price_source.get(product)
        return None

PriceManager in jedem Test neu zu erzeugen wird schnell repetitiv. Eine Fixture haelt das Setup an einer Stelle:

from decimal import Decimal
import pytest


@pytest.fixture()
def price_manager():
    return PriceManager(
        x_price_source=StubXPriceSource(Decimal("150.00")),
        y_price_source=StubYPriceSource(Decimal("220.00")),
    )


def test_get_price_x(price_manager):
    product = Product(type="x")
    assert price_manager.get_price(product) == Decimal("150.00")

Fixtures reduzieren Duplikation, machen Setup wiederverwendbar und koennen von anderen Fixtures abhaengen. Sie koennen auch verschiedene Scopes haben: function, class, module, package oder session.

Fixtures parametrisieren

Fixtures koennen ebenfalls parametrisiert werden:

import pytest


@pytest.fixture(params=[
    {"name": "Rashid", "age": 27},
    {"name": "Mamed", "age": 31},
])
def user(request):
    user_data = request.param
    user = UserModel(name=user_data["name"], age=user_data["age"])
    user.save()
    yield user
    user.delete()


def test_user_presentation(user):
    print(f"{user.name}, {user.age}")

Pytest fuehrt den Test zweimal aus, einmal fuer jeden Fixture-Wert.

Exceptions mit pytest.raises pruefen

Nutze pytest.raises, wenn das gewuenschte Verhalten eine Exception ist:

import pytest


def test_get_raises_error_when_not_found(my_repo):
    with pytest.raises(EntityDoesNotExistError) as exc_info:
        my_repo.get(999)

    assert "999" in str(exc_info.value)

So wird Fehlerverhalten explizit und testbar.

Externe Dependencies mocken

Mocks helfen, den getesteten Code von Remote Services, Datenbanken oder langsamen APIs zu isolieren.

def test_service_auth(mocker):
    transport_mock = mocker.Mock()
    transport_mock.get.side_effect = [
        HTTPAuthError("Unauthorized"),
        None,
        [{"name": "Alice"}, {"name": "Bob"}],
    ]

    client = MyHttpClient(transport=transport_mock)
    result = client.get_all_users()

    assert len(result) == 2

Wenn Dependency Injection nicht verfuegbar ist, kann Monkeypatching helfen:

def test_read_file(mocker):
    file_mock = mocker.mock_open(read_data="Hello, World!")
    mocker.patch("my_module.open", file_mock)

    got = FileReader.read("dummy.txt")
    assert got == "Hello, World!"

Nutze Patching bewusst. Es ist maechtig, aber Tests werden schwerer lesbar, wenn zu viele Internals ersetzt werden.

Yield Fixtures fuer Cleanup

Fixtures koennen Ressourcen nach dem Test aufraeumen:

@pytest.fixture()
def open_file():
    file_obj = open("test.txt", "w")
    yield file_obj
    file_obj.close()

Fuer komplexere Teardown-Logik kannst du request.addfinalizer verwenden:

@pytest.fixture()
def create_user(request):
    user = User(name="TempUser")
    user.save()

    def remove_user():
        user.delete()

    request.addfinalizer(remove_user)
    return user

yield Fixtures sind meistens leichter zu lesen, aber Finalizer sind nuetzlich, wenn Cleanup dynamisch registriert werden muss.

Key Takeaways

Schreibe regelmaessig Tests, damit Bugs sich nicht ansammeln.

Nutze Fixtures, um wiederholtes Setup zu entfernen und Tests leichter scanbar zu machen.

Nutze Marks, um plattformspezifische Tests zu ueberspringen, bekannte Fehler zu dokumentieren und Coverage zu parametrisieren.

Mocke externe Dependencies, wenn echte Services Tests langsam, flaky oder teuer machen wuerden.

Pytest bleibt eines der flexibelsten Testing Tools im Python Oekosystem. Auch 2026 ist es ein starker Default fuer Teams, die klare, wartbare und vertrauensbildende Tests wollen.