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.
📋 Table of Contents
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.
🔗 Share this article
✍️ Leave a Comment