Unit Testing in Python: Vollständiger Leitfaden mit Beispielen
Python-Tests mit pytest, TDD, Mocking und Abdeckung
Unit testing stellt sicher, dass Ihr Python-Code korrekt funktioniert und weiterhin funktioniert, wenn sich Ihr Projekt weiterentwickelt. Diese umfassende Anleitung behandelt alles, was Sie über Unit Testing in Python wissen müssen, von grundlegenden Konzepten bis hin zu fortgeschrittenen Techniken.

Warum Unit Testing wichtig ist
Unit Testing bietet zahlreiche Vorteile für Python-Entwickler:
- Frühe Fehlererkennung: Fangen Sie Fehler, bevor sie in die Produktion gelangen
- Code-Qualität: Erzwingt modularen, testbaren Code
- Refactoring-Konfidenz: Machen Sie Änderungen sicher, da Tests Regressionen erkennen
- Dokumentation: Tests dienen als ausführbare Dokumentation, wie der Code funktionieren sollte
- Schnellere Entwicklung: Automatisierte Tests sind schneller als manuelles Testen
- Bessere Architektur: Testbarer Code führt zu besserer Architektur
Grundlagen des Unit Testings verstehen
Was ist ein Unit Test?
Ein Unit Test überprüft den kleinsten testbaren Teil einer Anwendung (meist eine Funktion oder Methode) in Isolation. Er sollte:
- Schnell: In Millisekunden ausgeführt werden
- Isoliert: Unabhängig von anderen Tests und externen Systemen
- Wiederholbar: Gleiche Ergebnisse bei jeder Ausführung liefern
- Selbstvalidierend: Klare Erfolgs- oder Fehlermeldung ohne manuelle Überprüfung
- Zeitnah: Vor oder neben dem Code geschrieben werden
Die Test-Pyramide
Eine gesunde Test-Suite folgt der Test-Pyramide:
/\
/ \ E2E-Tests (Wenig)
/____\
/ \ Integrationstests (Einige)
/________\
/ \ Unit-Tests (Viele)
/____________\
Unit-Tests bilden die Grundlage - sie sind zahlreich, schnell und liefern schnelles Feedback.
Vergleich von Python-Testing-Frameworks
unittest: Das eingebaute Framework
Die Python-Standardbibliothek enthält unittest, inspiriert von JUnit:
import unittest
class TestCalculator(unittest.TestCase):
def setUp(self):
"""Wird vor jedem Test ausgeführt"""
self.calc = Calculator()
def tearDown(self):
"""Wird nach jedem Test ausgeführt"""
self.calc = None
def test_addition(self):
result = self.calc.add(2, 3)
self.assertEqual(result, 5)
def test_division_by_zero(self):
with self.assertRaises(ZeroDivisionError):
self.calc.divide(10, 0)
if __name__ == '__main__':
unittest.main()
Vorteile:
- Eingebaut, keine Installation erforderlich
- Gut für Entwickler, die mit xUnit-Frameworks vertraut sind
- Unternehmensfreundlich, gut etabliert
Nachteile:
- Umständliche Syntax mit Boilerplate-Code
- Erfordert Klassen für die Testorganisation
- Weniger flexible Fixture-Verwaltung
pytest: Die moderne Wahl
pytest ist das beliebteste Drittanbieter-Testing-Framework:
import pytest
def test_addition():
calc = Calculator()
assert calc.add(2, 3) == 5
def test_division_by_zero():
calc = Calculator()
with pytest.raises(ZeroDivisionError):
calc.divide(10, 0)
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
])
def test_addition_parametrized(a, b, expected):
calc = Calculator()
assert calc.add(a, b) == expected
Vorteile:
- Einfache, Python-idiomatische Syntax
- Leistungsfähiges Fixture-System
- Ausgezeichnetes Plugin-Ökosystem
- Bessere Fehlerberichterstattung
- Parametrisiertes Testen eingebaut
Nachteile:
- Installation erforderlich
- Weniger vertraut für Entwickler anderer Sprachen
Installation:
pip install pytest pytest-cov pytest-mock
Erstellen Ihrer ersten Unit Tests
Lassen Sie uns ein einfaches Beispiel von Grund auf mit Test-Driven Development (TDD) erstellen. Wenn Sie neu in Python sind oder eine schnelle Referenz für Syntax und Sprachmerkmale benötigen, werfen Sie einen Blick auf unseren Python Cheatsheet für einen umfassenden Überblick über Python-Grundlagen.
Beispiel: String-Utilitätsfunktionen
Schritt 1: Schreiben Sie den Test zuerst (Red)
Erstellen Sie test_string_utils.py:
import pytest
from string_utils import reverse_string, is_palindrome, count_vowels
def test_reverse_string():
assert reverse_string("hello") == "olleh"
assert reverse_string("") == ""
assert reverse_string("a") == "a"
def test_is_palindrome():
assert is_palindrome("racecar") == True
assert is_palindrome("hello") == False
assert is_palindrome("") == True
assert is_palindrome("A man a plan a canal Panama") == True
def test_count_vowels():
assert count_vowels("hello") == 2
assert count_vowels("HELLO") == 2
assert count_vowels("xyz") == 0
assert count_vowels("") == 0
Schritt 2: Schreiben Sie minimalen Code zum Bestehen (Green)
Erstellen Sie string_utils.py:
def reverse_string(s: str) -> str:
"""Ein String wird rückwärts geschrieben"""
return s[::-1]
def is_palindrome(s: str) -> bool:
"""Prüft, ob ein String ein Palindrom ist (Groß-/Kleinschreibung und Leerzeichen werden ignoriert)"""
cleaned = ''.join(s.lower().split())
return cleaned == cleaned[::-1]
def count_vowels(s: str) -> int:
"""Zählt die Vokale in einem String"""
return sum(1 for char in s.lower() if char in 'aeiou')
Schritt 3: Führen Sie die Tests aus
pytest test_string_utils.py -v
Ausgabe:
test_string_utils.py::test_reverse_string PASSED
test_string_utils.py::test_is_palindrome PASSED
test_string_utils.py::test_count_vowels PASSED
Fortgeschrittene Testtechniken
Verwendung von Fixtures für Testaufbau
Fixtures bieten wiederverwendbare Testaufbau- und -abbauprozeduren:
import pytest
from database import Database
@pytest.fixture
def db():
"""Erstellt eine Testdatenbank"""
database = Database(":memory:")
database.create_tables()
yield database # Wird dem Test bereitgestellt
database.close() # Aufräumen nach dem Test
@pytest.fixture
def sample_users(db):
"""Fügt Beispielbenutzer zur Datenbank hinzu"""
db.add_user("Alice", "alice@example.com")
db.add_user("Bob", "bob@example.com")
return db
def test_get_user(sample_users):
user = sample_users.get_user_by_email("alice@example.com")
assert user.name == "Alice"
def test_user_count(sample_users):
assert sample_users.count_users() == 2
Fixture-Bereiche
Steuern Sie den Lebenszyklus von Fixtures mit Bereichen:
@pytest.fixture(scope="function") # Standard: Wird für jeden Test ausgeführt
def func_fixture():
return create_resource()
@pytest.fixture(scope="class") # Einmal pro Testklasse
def class_fixture():
return create_resource()
@pytest.fixture(scope="module") # Einmal pro Modul
def module_fixture():
return create_resource()
@pytest.fixture(scope="session") # Einmal pro Test-Sitzung
def session_fixture():
return create_expensive_resource()
Mocking externer Abhängigkeiten
Verwenden Sie Mocking, um Code von externen Abhängigkeiten zu isolieren:
from unittest.mock import Mock, patch, MagicMock
import requests
class WeatherService:
def get_temperature(self, city):
response = requests.get(f"https://api.weather.com/{city}")
return response.json()["temp"]
# Test mit Mock
def test_get_temperature():
service = WeatherService()
# Mock die requests.get-Funktion
with patch('requests.get') as mock_get:
# Konfigurieren Sie die Mock-Antwort
mock_response = Mock()
mock_response.json.return_value = {"temp": 72}
mock_get.return_value = mock_response
# Test
temp = service.get_temperature("Boston")
assert temp == 72
# Überprüfen Sie den Aufruf
mock_get.assert_called_once_with("https://api.weather.com/Boston")
Verwendung des pytest-mock-Plugins
pytest-mock bietet eine sauberere Syntax:
def test_get_temperature(mocker):
service = WeatherService()
# Mock mit pytest-mock
mock_response = mocker.Mock()
mock_response.json.return_value = {"temp": 72}
mocker.patch('requests.get', return_value=mock_response)
temp = service.get_temperature("Boston")
assert temp == 72
Parametrisiertes Testen
Testen Sie mehrere Szenarien effizient:
import pytest
@pytest.mark.parametrize("input,expected", [
("", True),
("a", True),
("ab", False),
("aba", True),
("racecar", True),
("hello", False),
])
def test_is_palindrome_parametrized(input, expected):
assert is_palindrome(input) == expected
@pytest.mark.parametrize("number,is_even", [
(0, True),
(1, False),
(2, True),
(-1, False),
(-2, True),
])
def test_is_even(number, is_even):
assert (number % 2 == 0) == is_even
Testen von Ausnahmen und Fehlerbehandlung
import pytest
def divide(a, b):
if b == 0:
raise ValueError("Kann nicht durch null dividieren")
return a / b
def test_divide_by_zero():
with pytest.raises(ValueError, match="Kann nicht durch null dividieren"):
divide(10, 0)
def test_divide_by_zero_with_message():
with pytest.raises(ValueError) as exc_info:
divide(10, 0)
assert "null" in str(exc_info.value).lower()
# Testen, dass keine Ausnahme auftritt
def test_divide_success():
result = divide(10, 2)
assert result == 5.0
Testen von asynchronem Code
Das Testen asynchroner Code ist für moderne Python-Anwendungen essenziell, insbesondere bei der Arbeit mit APIs, Datenbanken oder KI-Diensten. Hier ist, wie Sie asynchrone Funktionen testen:
import pytest
import asyncio
async def fetch_data(url):
"""Asynchrone Funktion zum Abrufen von Daten"""
await asyncio.sleep(0.1) # Simuliert API-Aufruf
return {"status": "success"}
@pytest.mark.asyncio
async def test_fetch_data():
result = await fetch_data("https://api.example.com")
assert result["status"] == "success"
@pytest.mark.asyncio
async def test_fetch_data_with_mock(mocker):
# Mock die asynchrone Funktion
mock_fetch = mocker.AsyncMock(return_value={"status": "gemockt"})
mocker.patch('module.fetch_data', mock_fetch)
result = await fetch_data("https://api.example.com")
assert result["status"] == "gemockt"
Für praktische Beispiele zum Testen asynchroner Code mit KI-Diensten sehen Sie unsere Anleitung zur Integration von Ollama mit Python, die Teststrategien für LLM-Interaktionen umfasst.
Code Coverage
Messen Sie, wie viel Ihres Codes getestet wird:
Mit pytest-cov
# Führen Sie Tests mit Abdeckung aus
pytest --cov=myproject tests/
# Generieren Sie einen HTML-Bericht
pytest --cov=myproject --cov-report=html tests/
# Zeigen Sie fehlende Zeilen an
pytest --cov=myproject --cov-report=term-missing tests/
Coverage-Konfiguration
Erstellen Sie .coveragerc:
[run]
source = myproject
omit =
*/tests/*
*/venv/*
*/__pycache__/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
@abstractmethod
Coverage-Best Practices
- Streben Sie 80%+ Abdeckung für kritische Codepfade an
- Verfolgen Sie nicht 100% - konzentrieren Sie sich auf sinnvolle Tests
- Testen Sie Randfälle nicht nur glückliche Pfade
- Schließen Sie Boilerplate aus den Abdeckungsberichten aus
- Nutzen Sie die Abdeckung als Leitfaden nicht als Ziel
Testorganisation und Projektstruktur
Empfohlene Struktur
myproject/
├── myproject/
│ ├── __init__.py
│ ├── module1.py
│ ├── module2.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Geteilte Fixtures
│ ├── test_module1.py
│ ├── test_module2.py
│ ├── test_utils.py
│ └── integration/
│ ├── __init__.py
│ └── test_integration.py
├── pytest.ini
├── requirements.txt
└── requirements-dev.txt
conftest.py für Geteilte Fixtures
# tests/conftest.py
import pytest
from myproject.database import Database
@pytest.fixture(scope="session")
def test_db():
"""Sitzungsweite Testdatenbank"""
db = Database(":memory:")
db.create_schema()
yield db
db.close()
@pytest.fixture
def clean_db(test_db):
"""Saubere Datenbank für jeden Test"""
test_db.clear_all_tables()
return test_db
pytest.ini-Konfiguration
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--strict-markers
--cov=myproject
--cov-report=term-missing
--cov-report=html
markers =
slow: markiert Tests als langsam
integration: markiert Tests als Integrationstests
unit: markiert Tests als Unit-Tests
Best Practices für Unit-Tests
1. Folgen Sie dem AAA-Muster
Arrange-Act-Assert macht Tests klar:
def test_user_creation():
# Arrange
username = "john_doe"
email = "john@example.com"
# Act
user = User(username, email)
# Assert
assert user.username == username
assert user.email == email
assert user.is_active == True
2. Eine Assertion pro Test (Richtlinie, nicht Regel)
# Gut: Fokussierter Test
def test_user_username():
user = User("john_doe", "john@example.com")
assert user.username == "john_doe"
def test_user_email():
user = User("john_doe", "john@example.com")
assert user.email == "john@example.com"
# Auch akzeptabel: Verwandte Assertions
def test_user_creation():
user = User("john_doe", "john@example.com")
assert user.username == "john_doe"
assert user.email == "john@example.com"
assert isinstance(user.created_at, datetime)
3. Verwenden Sie beschreibende Testnamen
# Schlecht
def test_user():
pass
# Gut
def test_user_creation_with_valid_data():
pass
def test_user_creation_fails_with_invalid_email():
pass
def test_user_password_is_hashed_after_setting():
pass
4. Testen Sie Randfälle und Grenzen
def test_age_validation():
# Gültige Fälle
assert validate_age(0) == True
assert validate_age(18) == True
assert validate_age(120) == True
# Grenzfälle
assert validate_age(-1) == False
assert validate_age(121) == False
# Randfälle
with pytest.raises(TypeError):
validate_age("18")
with pytest.raises(TypeError):
validate_age(None)
5. Halten Sie Tests unabhängig
# Schlecht: Tests hängen von der Reihenfolge ab
counter = 0
def test_increment():
global counter
counter += 1
assert counter == 1
def test_increment_again(): # Fällt an, wenn allein ausgeführt
global counter
counter += 1
assert counter == 2
# Gut: Tests sind unabhängig
def test_increment():
counter = Counter()
counter.increment()
assert counter.value == 1
def test_increment_multiple_times():
counter = Counter()
counter.increment()
counter.increment()
assert counter.value == 2
6. Testen Sie keine Implementierungsdetails
# Schlecht: Implementierung testen
def test_sort_uses_quicksort():
sorter = Sorter()
assert sorter.algorithm == "quicksort"
# Gut: Verhalten testen
def test_sort_returns_sorted_list():
sorter = Sorter()
result = sorter.sort([3, 1, 2])
assert result == [1, 2, 3]
7. Testen Sie Echtwelt-Szenarien
Wenn Sie Bibliotheken testen, die Daten verarbeiten oder transformieren, konzentrieren Sie sich auf Echtwelt-Szenarien. Wenn Sie beispielsweise mit Web-Scraping oder Inhaltskonvertierung arbeiten, sehen Sie sich unsere Anleitung zur Konvertierung von HTML in Markdown mit Python an, die Teststrategien und Benchmark-Vergleiche für verschiedene Konvertierungsbibliotheken enthält.
Continuous Integration Integration
GitHub Actions-Beispiel
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, 3.10, 3.11, 3.12]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run tests
run: |
pytest --cov=myproject --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
Testen von Serverless-Funktionen
Beim Testen von AWS Lambda-Funktionen oder serverlosen Anwendungen sollten Sie Integrationsteststrategien neben Unit-Tests berücksichtigen. Unsere Anleitung zum Aufbau einer dualmodalen AWS Lambda mit Python und Terraform behandelt Testansätze für serverlose Python-Anwendungen, einschließlich der Testung von Lambda-Handlern, SQS-Verbrauchern und API-Gateway-Integrationen.
Testing Best Practices Checklist
- Schreiben Sie Tests vor oder neben dem Code (TDD)
- Halten Sie Tests schnell (< 1 Sekunde pro Test)
- Machen Sie Tests unabhängig und isoliert
- Verwenden Sie beschreibende Testnamen
- Folgen Sie dem AAA-Muster (Arrange-Act-Assert)
- Testen Sie Randfälle und Fehlerbedingungen
- Mocken Sie externe Abhängigkeiten
- Streben Sie 80%+ Code-Abdeckung an
- Führen Sie Tests im CI/CD-Pipeline aus
- Überprüfen und refaktorieren Sie Tests regelmäßig
- Dokumentieren Sie komplexe Test-Szenarien
- Verwenden Sie Fixtures für gemeinsame Einstellungen
- Parametrisieren Sie ähnliche Tests
- Halten Sie Tests einfach und lesbar
Häufige Testmuster
Testen von Klassen
class TestUser:
@pytest.fixture
def user(self):
return User("john_doe", "john@example.com")
def test_username(self, user):
assert user.username == "john_doe"
def test_email(self, user):
assert user.email == "john@example.com"
def test_full_name(self, user):
user.first_name = "John"
user.last_name = "Doe"
assert user.full_name() == "John Doe"
Testen mit temporären Dateien
import pytest
from pathlib import Path
@pytest.fixture
def temp_file(tmp_path):
"""Erstellen Sie eine temporäre Datei"""
file_path = tmp_path / "test_file.txt"
file_path.write_text("test content")
return file_path
def test_read_file(temp_file):
content = temp_file.read_text()
assert content == "test content"
Testen der Dateierstellung
Beim Testen von Code, der Dateien generiert (wie PDFs, Bilder oder Dokumente), verwenden Sie temporäre Verzeichnisse und überprüfen Sie die Dateieigenschaften:
@pytest.fixture
def temp_output_dir(tmp_path):
"""Bietet ein temporäres Ausgabeverzeichnis"""
output_dir = tmp_path / "output"
output_dir.mkdir()
return output_dir
def test_pdf_generation(temp_output_dir):
pdf_path = temp_output_dir / "output.pdf"
generate_pdf(pdf_path, content="Test")
assert pdf_path.exists()
assert pdf_path.stat().st_size > 0
Für umfassende Beispiele zum Testen der PDF-Erstellung sehen Sie sich unsere Anleitung zur Erstellung von PDFs in Python an, die Teststrategien für verschiedene PDF-Bibliotheken abdeckt.
Testen mit Monkeypatch
def test_environment_variable(monkeypatch):
monkeypatch.setenv("API_KEY", "test_key_123")
assert os.getenv("API_KEY") == "test_key_123"
def test_module_attribute(monkeypatch):
monkeypatch.setattr("module.CONSTANT", 42)
assert module.CONSTANT == 42
Nützliche Links und Ressourcen
- pytest Dokumentation
- unittest Dokumentation
- pytest-cov Plugin
- pytest-mock Plugin
- unittest.mock Dokumentation
- Test Driven Development by Example - Kent Beck
- Python Testing with pytest - Brian Okken
Verwandte Ressourcen
Python-Grundlagen und Best Practices
- Python Cheatsheet - Umfassende Referenz für Python-Syntax, Datenstrukturen und häufige Muster
Testen spezifischer Python-Anwendungsfälle
- Integration von Ollama mit Python - Teststrategien für LLM-Integrationen und asynchrone AI-Service-Aufrufe
- Konvertieren von HTML zu Markdown mit Python - Testansätze für Web-Scraping und Content-Konvertierungsbibliotheken
- Generieren von PDFs in Python - Teststrategien für PDF-Generierung und Datei-Ausgabe
Serverless- und Cloud-Testing
- Erstellen einer Dual-Modus-AWS-Lambda mit Python und Terraform - Testen von Lambda-Funktionen, SQS-Consumern und API-Gateway-Integrationen
Unit-Testing ist eine essentielle Fähigkeit für Python-Entwickler. Ob Sie sich für unittest oder pytest entscheiden, der Schlüssel liegt darin, Tests konsistent zu schreiben, sie wartbar zu halten und sie in Ihren Entwicklungsworkflow zu integrieren. Beginnen Sie mit einfachen Tests, übernehmen Sie schrittweise fortgeschrittene Techniken wie Mocking und Fixtures und nutzen Sie Abdeckungstools, um ungetesteten Code zu identifizieren.