pytest ist der Goldstandard für Python-Tests im Jahr 2026. Pytest wird von Django, FastAPI, NumPy und Tausenden von Open-Source-Projekten verwendet und macht das Schreiben und Ausführen von Tests intuitiv und leistungsstark. Dieser vollständige Leitfaden führt Sie vom ersten Test bis hin zu erweiterten Vorrichtungen, Parametrisierung und CI-Integration.
📋 Table of Contents
Warum Pytest über Unittest?
- Einfachere Syntax– schmucklos
assertAnweisungen mit hilfreichen Fehlermeldungen - Leistungsstarke Vorrichtungen– Abhängigkeitsinjektion mit automatischem Teardown
- Parametrisieren– Führen Sie einen Test mit vielen Eingaben durch
- Umfangreiches Plugin-Ökosystem– Abdeckung, xdist, Mock, Asyncio und mehr
- Bessere Entdeckung– findet Tests automatisch ohne Boilerplate
Installation und erster Test
# Install pytest
pip install pytest pytest-cov pytest-asyncio
# Create a test
# test_math.py
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
# Run tests
pytest # run all tests
pytest test_math.py # specific file
pytest -v # verbose output
pytest -k "add" # run tests matching name
pytest --tb=short # shorter tracebacks
Testorganisation
# Recommended structure
# tests/
# conftest.py — shared fixtures
# test_users.py
# test_orders.py
# integration/
# test_api.py
# unit/
# test_services.py
# pytest.ini or pyproject.toml
# [tool.pytest.ini_options]
# testpaths = ["tests"]
# addopts = "-v --tb=short"
# Test class grouping
class TestUserService:
def test_create_user(self):
user = create_user("alice@example.com")
assert user.email == "alice@example.com"
def test_create_user_invalid_email(self):
with pytest.raises(ValueError, match="Invalid email"):
create_user("not-an-email")
def test_duplicate_email_raises(self, db):
create_user("alice@example.com")
with pytest.raises(DuplicateError):
create_user("alice@example.com")
Fixtures – die Supermacht von Pytest
import pytest
from app.database import Database
from app.models import User
# Simple fixture
@pytest.fixture
def sample_user():
return User(id=1, name="Alice", email="alice@example.com")
# Fixture with setup and teardown
@pytest.fixture
def db():
database = Database(url="sqlite:///:memory:")
database.create_tables()
yield database # test runs here
database.drop_tables() # teardown
# Session-scoped fixture — runs once per test session
@pytest.fixture(scope="session")
def app_client():
from app import create_app
app = create_app(testing=True)
return app.test_client()
# Fixture using other fixtures
@pytest.fixture
def admin_user(db):
user = db.create_user(email="admin@test.com", role="admin")
return user
# conftest.py — shared fixtures auto-discovered
# pytest finds conftest.py automatically
Parametrisieren – Mehrere Fälle testen
import pytest
def is_valid_email(email: str) -> bool:
import re
return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))
@pytest.mark.parametrize("email,expected", [
("alice@example.com", True),
("user+tag@domain.co.uk", True),
("invalid", False),
("no-at-sign", False),
("@nodomain.com", False),
("user@.com", False),
("", False),
])
def test_email_validation(email, expected):
assert is_valid_email(email) == expected
# Multiple parametrize decorators — creates cartesian product
@pytest.mark.parametrize("base", [2, 10, 16])
@pytest.mark.parametrize("exp", [1, 2, 3])
def test_powers(base, exp):
result = base ** exp
assert result == pow(base, exp)
Verspotten mit Pytest-Mock
pip install pytest-mock
import pytest
from app.services import UserService
from app.email import EmailSender
def test_registration_sends_welcome_email(mocker):
# Mock the email sender
mock_send = mocker.patch("app.services.EmailSender.send")
service = UserService()
service.register("alice@example.com", "securepass")
# Assert email was sent
mock_send.assert_called_once_with(
to="alice@example.com",
subject="Welcome to TechPulse!"
)
def test_external_api_timeout(mocker):
# Mock requests.get to simulate timeout
mocker.patch(
"requests.get",
side_effect=requests.exceptions.Timeout
)
with pytest.raises(ServiceUnavailableError):
fetch_weather("London")
# Spy on real method (calls real code but tracks calls)
def test_cache_used_on_second_call(mocker):
spy = mocker.spy(cache_service, "get")
fetch_user(1)
fetch_user(1) # should hit cache
assert spy.call_count == 2
Async-Code testen
pip install pytest-asyncio
import pytest
import pytest_asyncio
# Mark entire file as async
# pytest.ini: asyncio_mode = auto
@pytest.mark.asyncio
async def test_async_fetch():
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.get("/api/users")
assert response.status_code == 200
# Async fixture
@pytest_asyncio.fixture
async def async_db():
async with AsyncDatabase() as db:
await db.create_tables()
yield db
await db.drop_tables()
Berichterstattung über die Abdeckung
# Run with coverage
pytest --cov=app --cov-report=html --cov-report=term-missing
# Output:
# Name Stmts Miss Cover
# app/models.py 45 3 93%
# app/services.py 87 12 86%
# TOTAL 132 15 89%
# Fail if coverage below threshold
pytest --cov=app --cov-fail-under=80
# .coveragerc or pyproject.toml
# [tool.coverage.run]
# omit = ["*/tests/*", "*/migrations/*"]
pytest Marks – Tests kategorisieren
import pytest
# Define marks in pyproject.toml
# [tool.pytest.ini_options]
# markers = [
# "slow: marks tests as slow",
# "integration: marks integration tests",
# "unit: marks unit tests",
# ]
@pytest.mark.slow
@pytest.mark.integration
def test_full_user_registration_flow(client, db):
# End-to-end registration test
response = client.post("/register", json={
"email": "test@example.com",
"password": "SecurePass123!"
})
assert response.status_code == 201
assert db.users.count() == 1
# Run only unit tests
# pytest -m unit
# Skip slow tests in CI
# pytest -m "not slow"
GitHub Actions CI-Integration
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install uv
uv pip install -e ".[dev]" --system
- name: Run tests
run: pytest --cov=app --cov-fail-under=80
- name: Upload coverage
uses: codecov/codecov-action@v4
Die Beherrschung von Pytest erfordert Übung, zahlt sich aber aus. Schreiben Sie Tests neben Features (TDD), verwenden Sie Fixtures zum Auf- und Abbau, parametrisieren Sie für Randfälle und simulieren Sie externe Abhängigkeiten. Streben Sie eine Abdeckung von über 80 % der Geschäftslogik und 100 % der kritischen Pfade an.
🔗 Share this article
✍️ Leave a Comment