🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

Best Practices für Python-Tests 2026: Hypothese, Mocking und Coverage

⏱️5 min read  ·  947 words

Gute Tests zu schreiben ist eine Fähigkeit eines erfahrenen Entwicklers. Über die grundlegende Einrichtung von Pytests hinaus erfordert das Testen von Python in der Produktion im Jahr 2026 ein Verständnis der Testarchitektur, von Fixtures, Mocking-Strategien, eigenschaftsbasierten Tests und der CI-Integration. Dieser Leitfaden behandelt erweiterte Testmuster, die in professionellen Python-Projekten verwendet werden.

Testarchitektur: Die Testpyramide

Testing Pyramid:

         /   E2E   \       - Few, slow, expensive
        /  Integration \   - Some, moderate
       /  Unit Tests    \  - Many, fast, cheap

Rule: 70% unit, 20% integration, 10% E2E

Good test properties:
- Fast: unit tests < 1ms each
- Independent: no shared state between tests
- Repeatable: same result every run
- Self-validating: clear pass/fail
- Timely: written alongside/before code

Erweiterte Vorrichtungen

import pytest
import asyncio
from unittest.mock import MagicMock, AsyncMock, patch
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine

# Fixture scope hierarchy
@pytest.fixture(scope="session")
def event_loop():
    loop = asyncio.new_event_loop()
    yield loop
    loop.close()

@pytest.fixture(scope="session")
async def db_engine():
    engine = create_async_engine("sqlite+aiosqlite:///:memory:")
    await create_all_tables(engine)
    yield engine
    await engine.dispose()

@pytest.fixture
async def db(db_engine):
    async with AsyncSession(db_engine) as session:
        yield session
        await session.rollback()  # clean state after each test

@pytest.fixture
def mock_email_service():
    with patch("app.services.email.EmailService.send") as mock:
        mock.return_value = True
        yield mock

# Factory fixture pattern
@pytest.fixture
def make_user(db):
    async def _factory(email="test@example.com", role="user", **kwargs):
        user = User(email=email, role=role, **kwargs)
        db.add(user)
        await db.commit()
        return user
    return _factory

# Usage
async def test_user_creation(db, make_user, mock_email_service):
    user = await make_user(email="alice@example.com")
    assert user.id is not None
    mock_email_service.assert_called_once_with(
        to="alice@example.com",
        subject="Welcome!"
    )

Eigenschaftsbasiertes Testen mit Hypothese

pip install hypothesis

from hypothesis import given, settings, strategies as st
from hypothesis.extra.pandas import data_frames, column

# Test with arbitrary inputs
@given(st.integers(), st.integers())
def test_add_commutative(a: int, b: int):
    assert add(a, b) == add(b, a)

@given(st.lists(st.integers(), min_size=1))
def test_sort_preserves_length(lst: list[int]):
    sorted_lst = sorted(lst)
    assert len(sorted_lst) == len(lst)

@given(st.text(min_size=1, max_size=100))
def test_hash_always_returns_string(text: str):
    result = hash_password(text)
    assert isinstance(result, str)
    assert len(result) > 0

# Complex data generation
@given(
    st.builds(
        CreateUserRequest,
        email=st.emails(),
        name=st.text(min_size=2, max_size=50).filter(str.strip),
        password=st.text(min_size=8, max_size=128),
    )
)
@settings(max_examples=50)
def test_user_creation_accepts_valid_data(request: CreateUserRequest):
    user = create_user(request)
    assert user.email == request.email.lower()

# Stateful testing
from hypothesis.stateful import RuleBasedStateMachine, rule, invariant

class QueueMachine(RuleBasedStateMachine):
    def __init__(self):
        super().__init__()
        self.model = []
        self.implementation = Queue()

    @rule(value=st.integers())
    def enqueue(self, value: int):
        self.model.append(value)
        self.implementation.enqueue(value)

    @rule()
    def dequeue(self):
        if self.model:
            expected = self.model.pop(0)
            actual = self.implementation.dequeue()
            assert actual == expected

    @invariant()
    def sizes_match(self):
        assert len(self.model) == self.implementation.size()

Best Practices verspotten

from unittest.mock import MagicMock, AsyncMock, patch, call
import pytest

# Mock at the right level — where it's USED, not where it's defined
# If service.py imports: from utils import send_email
# Mock: "service.send_email" not "utils.send_email"

def test_order_sends_confirmation(mocker):
    # Mock async method
    mock_send = mocker.patch("app.services.order.send_email", new=AsyncMock())
    mock_send.return_value = True

    order = asyncio.run(create_order(user_id=1, items=[...]))

    mock_send.assert_called_once()
    call_kwargs = mock_send.call_args.kwargs
    assert call_kwargs["to"] == "alice@example.com"
    assert "order confirmation" in call_kwargs["subject"].lower()

# Mock with side effects
def test_retry_on_network_error(mocker):
    mock_get = mocker.patch("requests.get")
    mock_get.side_effect = [
        ConnectionError("Network error"),  # first call fails
        ConnectionError("Network error"),  # second call fails
        MagicMock(json=lambda: {"data": "success"}),  # third succeeds
    ]

    result = fetch_with_retry("https://api.example.com")
    assert result == {"data": "success"}
    assert mock_get.call_count == 3

# Context manager mock
def test_file_processing(mocker):
    mock_open = mocker.mock_open(read_data="line1
line2
line3")
    mocker.patch("builtins.open", mock_open)

    result = count_lines("any_file.txt")
    assert result == 3

Integrationstests mit realen Diensten

# Using pytest-docker for real database in CI
import pytest
import psycopg2
from testcontainers.postgres import PostgresContainer

@pytest.fixture(scope="session")
def postgres():
    with PostgresContainer("postgres:16") as pg:
        yield pg

@pytest.fixture(scope="session")
def db_url(postgres):
    return postgres.get_connection_url()

# Using respx to mock HTTP at socket level
import respx
import httpx

@pytest.mark.asyncio
async def test_external_api_integration():
    with respx.mock:
        respx.get("https://api.github.com/users/alice").mock(
            return_value=httpx.Response(200, json={"login": "alice", "followers": 1000})
        )
        result = await fetch_github_user("alice")
        assert result["followers"] == 1000

Testabdeckung und Qualitätsmetriken

# pytest.ini or pyproject.toml
# [tool.pytest.ini_options]
# addopts = "--cov=app --cov-report=html --cov-report=term-missing --cov-fail-under=80"

# Run with coverage
pytest --cov=app --cov-report=html

# Mutation testing (verify your tests actually catch bugs)
pip install mutmut
mutmut run --paths-to-mutate app/services/
mutmut results  # shows surviving mutants (untested code paths)

# Performance benchmarks
pip install pytest-benchmark

@pytest.mark.benchmark
def test_encryption_performance(benchmark):
    result = benchmark(encrypt_password, "test_password")
    assert len(result) > 0  # benchmark.stats shows timing

Best Practices für Python-Tests im Jahr 2026: Schreiben Sie Tests parallel zum Code, verwenden Sie Factory-Fixtures für Testdaten, simulieren Sie die Grenze, die nicht innerhalb der Implementierung liegt, verwenden Sie eigenschaftsbasierte Tests zur Datenvalidierung und messen Sie den Mutations-Score, um zu überprüfen, ob Tests tatsächlich Fehler erkennen. Eine Abdeckung von 80 % ist der Boden, nicht die Decke – hochwertige Pfade sollten 100 % betragen.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা