Meistern Sie Pythons unittest.mock-Bibliothek. Ein tiefer Einblick in Test-Doubles, Mock-Objekte, Stubs, Fakes und den Patch-Decorator für robuste, isolierte Unit-Tests.
Python Mock-Objekte: Ein umfassender Leitfaden zur Implementierung von Test-Doubles
In der Welt der modernen Softwareentwicklung ist das Schreiben von Code nur die halbe Miete. Sicherzustellen, dass dieser Code zuverlässig, robust ist und wie erwartet funktioniert, ist die andere, ebenso wichtige Hälfte. Hier kommen automatisierte Tests ins Spiel. Insbesondere Unit-Tests sind eine grundlegende Praxis, bei der einzelne Komponenten oder 'Einheiten' einer Anwendung isoliert getestet werden. Diese Isolation ist jedoch oft leichter gesagt als getan. Reale Anwendungen sind komplexe Geflechte aus miteinander verbundenen Objekten, Diensten und externen Systemen. Wie können Sie eine einzelne Funktion testen, wenn sie von einer Datenbank, einer Drittanbieter-API oder einem anderen komplexen Teil Ihres Systems abhängt?
Die Antwort liegt in einer leistungsstarken Technik: der Verwendung von Test-Doubles. Und im Python-Ökosystem ist das primäre Werkzeug zu ihrer Erstellung die vielseitige und unverzichtbare unittest.mock-Bibliothek. Dieser Leitfaden führt Sie tief in die Welt der Mocks und Test-Doubles in Python ein. Wir werden das 'Warum' dahinter erforschen, die verschiedenen Typen entmystifizieren und praktische, reale Beispiele mit unittest.mock liefern, die Ihnen helfen, sauberere, schnellere und effektivere Tests zu schreiben.
Was sind Test-Doubles und warum brauchen wir sie?
Stellen Sie sich vor, Sie entwickeln eine Funktion, die das Profil eines Benutzers aus der Datenbank Ihres Unternehmens abruft und es dann formatiert. Die Funktionssignatur könnte so aussehen: get_formatted_user_profile(user_id, db_connection).
Um diese Funktion zu testen, stehen Sie vor mehreren Herausforderungen:
- Abhängigkeit von einem Live-System: Ihr Test würde eine laufende Datenbank benötigen. Das macht Tests langsam, aufwendig in der Einrichtung und abhängig vom Zustand und der Verfügbarkeit eines externen Systems.
- Unvorhersehbarkeit: Die Daten in der Datenbank könnten sich ändern, was dazu führt, dass Ihr Test fehlschlägt, obwohl Ihre Formatierungslogik korrekt ist. Das macht Tests 'flaky' oder nicht-deterministisch.
- Schwierigkeiten beim Testen von Grenzfällen: Wie würden Sie testen, was passiert, wenn die Datenbankverbindung fehlschlägt oder wenn sie einen Benutzer zurückgibt, bei dem einige Daten fehlen? Die Simulation dieser spezifischen Szenarien mit einer echten Datenbank kann unglaublich schwierig sein.
Ein Test-Double ist ein Oberbegriff für jedes Objekt, das während eines Tests als Platzhalter für ein echtes Objekt dient. Indem wir die echte db_connection durch ein Test-Double ersetzen, können wir die Abhängigkeit von der tatsächlichen Datenbank aufheben und die volle Kontrolle über die Testumgebung übernehmen.
Die Verwendung von Test-Doubles bietet mehrere entscheidende Vorteile:
- Isolation: Sie ermöglichen es Ihnen, Ihre Code-Einheit (z. B. die Formatierungslogik) vollständig isoliert von ihren Abhängigkeiten (z. B. der Datenbank) zu testen. Wenn der Test fehlschlägt, wissen Sie, dass das Problem in der zu testenden Einheit liegt und nicht anderswo.
- Geschwindigkeit: Das Ersetzen langsamer Operationen wie Netzwerkanfragen oder Datenbankabfragen durch ein In-Memory-Test-Double beschleunigt Ihre Test-Suite dramatisch. Schnelle Tests werden häufiger ausgeführt, was zu einer engeren Feedback-Schleife für Entwickler führt.
- Determinismus: Sie können das Test-Double so konfigurieren, dass es bei jeder Ausführung des Tests vorhersagbare Daten zurückgibt. Dies eliminiert 'flaky' Tests und stellt sicher, dass ein fehlschlagender Test auf ein echtes Problem hinweist.
- Möglichkeit zum Testen von Grenzfällen: Sie können ein Double leicht so konfigurieren, dass es Fehlerbedingungen simuliert, wie das Auslösen eines
ConnectionErroroder die Rückgabe leerer Daten, sodass Sie überprüfen können, ob Ihr Code diese Situationen ordnungsgemäß behandelt.
Die Taxonomie der Test-Doubles: Mehr als nur „Mocks“
Obwohl Entwickler den Begriff „Mock“ oft allgemein für jedes Test-Double verwenden, ist es hilfreich, die präzisere Terminologie zu verstehen, die von Gerard Meszaros in seinem Buch „xUnit Test Patterns“ geprägt wurde. Das Wissen um diese Unterscheidungen hilft Ihnen, klarer darüber nachzudenken, was Sie in Ihrem Test erreichen wollen.
1. Dummy
Ein Dummy-Objekt ist das einfachste Test-Double. Es wird herumgereicht, um eine Parameterliste zu füllen, wird aber nie tatsächlich verwendet. Seine Methoden werden normalerweise nicht aufgerufen. Sie verwenden einen Dummy, wenn Sie einer Methode ein Argument übergeben müssen, sich aber im Kontext des spezifischen Tests nicht für das Verhalten dieses Arguments interessieren.
Beispiel: Wenn eine Funktion ein 'logger'-Objekt benötigt, Ihr Test sich aber nicht darum kümmert, was protokolliert wird, könnten Sie ein Dummy-Objekt übergeben.
2. Fake
Ein Fake-Objekt hat eine funktionierende Implementierung, aber es ist eine viel einfachere Version des Produktionsobjekts. Es verwendet keine externen Ressourcen und ersetzt eine schwergewichtige Implementierung durch eine leichtgewichtige. Das klassische Beispiel ist eine In-Memory-Datenbank, die eine echte Datenbankverbindung ersetzt. Sie funktioniert tatsächlich – man kann Daten hinzufügen und daraus lesen – aber unter der Haube ist es nur ein einfaches Dictionary oder eine Liste.
3. Stub
Ein Stub liefert vorprogrammierte, „konservierte“ Antworten auf Methodenaufrufe, die während eines Tests gemacht werden. Er wird verwendet, wenn Ihr Code spezifische Daten von einer Abhängigkeit empfangen muss. Zum Beispiel könnten Sie eine Methode wie api_client.get_user(user_id=123) so stubben, dass sie immer ein bestimmtes Benutzer-Dictionary zurückgibt, ohne tatsächlich einen API-Aufruf zu tätigen.
4. Spy
Ein Spy ist ein Stub, der auch Informationen darüber aufzeichnet, wie er aufgerufen wurde. Zum Beispiel könnte er aufzeichnen, wie oft eine Methode aufgerufen wurde oder welche Argumente an sie übergeben wurden. Dies ermöglicht es Ihnen, die Interaktion zwischen Ihrem Code und seiner Abhängigkeit zu „bespitzeln“ (to spy on) und dann im Nachhinein Zusicherungen über diese Interaktion zu machen.
5. Mock
Ein Mock ist der „bewussteste“ Typ eines Test-Doubles. Es ist ein Objekt, das mit Erwartungen vorprogrammiert ist, welche Methoden aufgerufen werden, mit welchen Argumenten und in welcher Reihenfolge. Ein Test, der ein Mock-Objekt verwendet, schlägt typischerweise nicht nur fehl, wenn der zu testende Code das falsche Ergebnis liefert, sondern auch, wenn er nicht auf die genau erwartete Weise mit dem Mock interagiert. Mocks eignen sich hervorragend zur Verhaltensüberprüfung – um sicherzustellen, dass eine bestimmte Abfolge von Aktionen stattgefunden hat.
Pythons unittest.mock-Bibliothek bietet eine einzige, leistungsstarke Klasse, die je nach Verwendung als Stub, Spy oder Mock fungieren kann.
Einführung in Pythons Kraftpaket: Die `unittest.mock`-Bibliothek
Als Teil der Python-Standardbibliothek seit Version 3.3 ist unittest.mock die kanonische Lösung zur Erstellung von Test-Doubles. Ihre Flexibilität und Leistungsfähigkeit machen sie zu einem unverzichtbaren Werkzeug für jeden ernsthaften Python-Entwickler. Wenn Sie eine ältere Version von Python verwenden, können Sie die zurückportierte Bibliothek über pip installieren: pip install mock.
Der Kern der Bibliothek dreht sich um zwei Schlüsselklassen: Mock und ihr fähigeres Geschwister, MagicMock. Diese Objekte sind so konzipiert, dass sie unglaublich flexibel sind und Attribute und Methoden im laufenden Betrieb erstellen, wenn Sie darauf zugreifen.
Tiefer Einblick: Die Klassen `Mock` und `MagicMock`
Das `Mock`-Objekt
Ein `Mock`-Objekt ist ein Chamäleon. Sie können eines erstellen, und es wird sofort auf jeden Attributzugriff oder Methodenaufruf reagieren und standardmäßig ein anderes Mock-Objekt zurückgeben. Dies ermöglicht es Ihnen, Aufrufe während der Einrichtung einfach zu verketten.
# In einer Testdatei...
from unittest.mock import Mock
# Ein Mock-Objekt erstellen
mock_api = Mock()
# Der Zugriff auf ein Attribut erstellt es und gibt ein weiteres Mock zurück
print(mock_api.users)
# Ausgabe: <Mock name='mock.users' id='...'>
# Das Aufrufen einer Methode gibt standardmäßig ebenfalls ein Mock zurück
print(mock_api.users.get(id=1))
# Ausgabe: <Mock name='mock.users.get()' id='...'>
Dieses Standardverhalten ist für das Testen nicht sehr nützlich. Die wahre Stärke liegt darin, den Mock so zu konfigurieren, dass er sich wie das Objekt verhält, das er ersetzt.
Konfiguration von Rückgabewerten und Seiteneffekten
Sie können einer Mock-Methode über das Attribut return_value mitteilen, was sie zurückgeben soll. Auf diese Weise erstellen Sie einen Stub.
from unittest.mock import Mock
# Einen Mock für einen Datendienst erstellen
mock_service = Mock()
# Den Rückgabewert für einen Methodenaufruf konfigurieren
mock_service.get_data.return_value = {'id': 1, 'name': 'Test Data'}
# Wenn wir sie jetzt aufrufen, erhalten wir unseren konfigurierten Wert
result = mock_service.get_data()
print(result)
# Ausgabe: {'id': 1, 'name': 'Test Data'}
Um Fehler zu simulieren, können Sie das Attribut side_effect verwenden. Dies ist perfekt, um die Fehlerbehandlung Ihres Codes zu testen.
from unittest.mock import Mock
mock_service = Mock()
# Die Methode so konfigurieren, dass sie eine Ausnahme auslöst
mock_service.get_data.side_effect = ConnectionError("Failed to connect to service")
# Der Aufruf der Methode wird nun die Ausnahme auslösen
try:
mock_service.get_data()
except ConnectionError as e:
print(e)
# Ausgabe: Failed to connect to service
Assertion-Methoden zur Überprüfung
Mock-Objekte fungieren auch als Spies und Mocks, indem sie aufzeichnen, wie sie verwendet werden. Sie können dann eine Reihe von integrierten Assertion-Methoden verwenden, um diese Interaktionen zu überprüfen.
mock_object.method.assert_called(): Stellt sicher, dass die Methode mindestens einmal aufgerufen wurde.mock_object.method.assert_called_once(): Stellt sicher, dass die Methode genau einmal aufgerufen wurde.mock_object.method.assert_called_with(*args, **kwargs): Stellt sicher, dass die Methode zuletzt mit den angegebenen Argumenten aufgerufen wurde.mock_object.method.assert_any_call(*args, **kwargs): Stellt sicher, dass die Methode zu irgendeinem Zeitpunkt mit diesen Argumenten aufgerufen wurde.mock_object.method.assert_not_called(): Stellt sicher, dass die Methode nie aufgerufen wurde.mock_object.call_count: Eine Ganzzahl-Eigenschaft, die Ihnen sagt, wie oft die Methode aufgerufen wurde.
from unittest.mock import Mock
mock_notifier = Mock()
# Stellen Sie sich vor, dies ist unsere zu testende Funktion
def process_and_notify(data, notifier):
if data.get('critical'):
notifier.send_alert(message="Critical event occurred!")
# Testfall 1: Kritische Daten
process_and_notify({'critical': True}, mock_notifier)
mock_notifier.send_alert.assert_called_once_with(message="Critical event occurred!")
# Den Mock für den nächsten Test zurücksetzen
mock_notifier.reset_mock()
# Testfall 2: Nicht-kritische Daten
process_and_notify({'critical': False}, mock_notifier)
mock_notifier.send_alert.assert_not_called()
Das `MagicMock`-Objekt
Ein `MagicMock` ist eine Unterklasse von `Mock` mit einem entscheidenden Unterschied: Es hat Standardimplementierungen für die meisten von Pythons „magischen“ oder „Dunder“-Methoden (z. B. __len__, __str__, __iter__). Wenn Sie versuchen, ein reguläres `Mock` in einem Kontext zu verwenden, der eine dieser Methoden erfordert, erhalten Sie einen Fehler.
from unittest.mock import Mock, MagicMock
# Verwendung eines regulären Mock
mock_list = Mock()
try:
len(mock_list)
except TypeError as e:
print(e) # Ausgabe: 'Mock' object has no len()
# Verwendung eines MagicMock
magic_mock_list = MagicMock()
print(len(magic_mock_list)) # Ausgabe: 0 (standardmäßig)
# Wir können auch den Rückgabewert der magischen Methode konfigurieren
magic_mock_list.__len__.return_value = 100
print(len(magic_mock_list)) # Ausgabe: 100
Faustregel: Beginnen Sie mit `MagicMock`. Es ist im Allgemeinen sicherer und deckt mehr Anwendungsfälle ab, wie zum Beispiel das Mocken von Objekten, die in for-Schleifen (erfordert __iter__) oder with-Anweisungen (erfordert __enter__ und __exit__) verwendet werden.
Praktische Implementierung: Der `patch`-Decorator und Kontext-Manager
Einen Mock zu erstellen ist eine Sache, aber wie bringen Sie Ihren Code dazu, ihn anstelle des echten Objekts zu verwenden? Hier kommt `patch` ins Spiel. `patch` ist ein leistungsstarkes Werkzeug in `unittest.mock`, das ein Zielobjekt für die Dauer eines Tests vorübergehend durch einen Mock ersetzt.
`@patch` als Decorator
Die gebräuchlichste Art, `patch` zu verwenden, ist als Decorator für Ihre Testmethode. Sie geben den String-Pfad zu dem Objekt an, das Sie ersetzen möchten.
Nehmen wir an, wir haben eine Funktion, die Daten von einer Web-API mit der beliebten `requests`-Bibliothek abruft:
# in Datei: my_app/data_fetcher.py
import requests
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status() # Löst eine Ausnahme für schlechte Statuscodes aus
return response.json()
Wir möchten diese Funktion testen, ohne einen echten Netzwerkaufruf zu tätigen. Wir können `requests.get` patchen:
# in Datei: tests/test_data_fetcher.py
import unittest
from unittest.mock import patch, Mock
from my_app.data_fetcher import get_user_data
class TestDataFetcher(unittest.TestCase):
@patch('my_app.data_fetcher.requests.get')
def test_get_user_data_success(self, mock_get):
"""Testet erfolgreiches Abrufen von Daten."""
# Konfigurieren Sie den Mock, um eine erfolgreiche API-Antwort zu simulieren
mock_response = Mock()
mock_response.json.return_value = {'id': 1, 'name': 'John Doe'}
mock_response.raise_for_status.return_value = None # Bei Erfolg nichts tun
mock_get.return_value = mock_response
# Rufen Sie unsere Funktion auf
user_data = get_user_data(1)
# Stellen Sie sicher, dass unsere Funktion den korrekten API-Aufruf gemacht hat
mock_get.assert_called_once_with('https://api.example.com/users/1')
# Stellen Sie sicher, dass unsere Funktion die erwarteten Daten zurückgegeben hat
self.assertEqual(user_data, {'id': 1, 'name': 'John Doe'})
Beachten Sie, wie `patch` ein `MagicMock` erstellt und es als Argument `mock_get` an unsere Testmethode übergibt. Innerhalb des Tests wird jeder Aufruf von `requests.get` in `my_app.data_fetcher` an unser Mock-Objekt umgeleitet.
`patch` als Kontext-Manager
Manchmal müssen Sie etwas nur für einen kleinen Teil eines Tests patchen. Die Verwendung von `patch` als Kontext-Manager mit einer `with`-Anweisung ist dafür perfekt.
# in Datei: tests/test_data_fetcher.py
import unittest
from unittest.mock import patch, Mock
from my_app.data_fetcher import get_user_data
class TestDataFetcher(unittest.TestCase):
def test_get_user_data_with_context_manager(self):
"""Testet die Verwendung von patch als Kontext-Manager."""
with patch('my_app.data_fetcher.requests.get') as mock_get:
# Konfigurieren Sie den Mock innerhalb des 'with'-Blocks
mock_response = Mock()
mock_response.json.return_value = {'id': 2, 'name': 'Jane Doe'}
mock_get.return_value = mock_response
user_data = get_user_data(2)
mock_get.assert_called_once_with('https://api.example.com/users/2')
self.assertEqual(user_data, {'id': 2, 'name': 'Jane Doe'})
# Außerhalb des 'with'-Blocks ist requests.get wieder im Originalzustand
Ein entscheidendes Konzept: Wo patchen?
Dies ist die häufigste Quelle der Verwirrung bei der Verwendung von `patch`. Die Regel lautet: Sie müssen das Objekt dort patchen, wo es nachgeschlagen wird, nicht dort, wo es definiert ist.
Lassen Sie uns dies mit einem Beispiel veranschaulichen. Angenommen, wir haben zwei Dateien:
# in Datei: services.py
class Database:
def connect(self):
# ... komplexe Verbindungslogik ...
return "REAL_CONNECTION"
# in Datei: main_app.py
from services import Database
def start_app():
db = Database()
connection = db.connect()
print(f"Got connection: {connection}")
return connection
Jetzt möchten wir `start_app` in `main_app.py` testen, ohne ein echtes `Database`-Objekt zu erstellen. Ein häufiger Fehler ist der Versuch, `services.Database` zu patchen.
# in Datei: test_main_app.py
import unittest
from unittest.mock import patch
from main_app import start_app
class TestApp(unittest.TestCase):
# DIES IST DER FALSCHE WEG ZUM PATCHEN!
@patch('services.Database')
def test_start_app_incorrectly(self, mock_db):
start_app()
# Dieser Test wird immer noch die ECHTE Database-Klasse verwenden!
# DIES IST DER KORREKTE WEG ZUM PATCHEN!
@patch('main_app.Database')
def test_start_app_correctly(self, mock_db_class):
# Wir patchen 'Database' im 'main_app'-Namensraum
# Konfigurieren Sie die Mock-Instanz, die erstellt wird
mock_instance = mock_db_class.return_value
mock_instance.connect.return_value = "MOCKED_CONNECTION"
connection = start_app()
# Stellen Sie sicher, dass unser Mock verwendet wurde
mock_db_class.assert_called_once() # Wurde die Klasse instanziiert?
mock_instance.connect.assert_called_once() # Wurde die connect-Methode aufgerufen?
self.assertEqual(connection, "MOCKED_CONNECTION")
Warum schlägt der erste Test fehl? Weil `main_app.py` `from services import Database` ausführt. Dies importiert die `Database`-Klasse in den Namensraum des `main_app`-Moduls. Wenn `start_app` läuft, sucht es nach `Database` in seinem eigenen Modul (`main_app`). Das Patchen von `services.Database` ändert es im `services`-Modul, aber `main_app` hat bereits eine eigene Referenz auf die ursprüngliche Klasse. Der korrekte Ansatz ist, `main_app.Database` zu patchen, was der Name ist, den der zu testende Code tatsächlich verwendet.
Fortgeschrittene Mocking-Techniken
`spec` und `autospec`: Mocks sicherer machen
Ein Standard-`MagicMock` hat einen potenziellen Nachteil: Es erlaubt Ihnen, jede Methode mit beliebigen Argumenten aufzurufen, auch wenn diese Methode auf dem echten Objekt nicht existiert. Dies kann zu Tests führen, die bestehen, aber echte Probleme verbergen, wie Tippfehler in Methodennamen oder Änderungen in der API eines realen Objekts.
# Echte Klasse
class Notifier:
def send_message(self, text):
# ... sendet Nachricht ...
pass
# Ein Test mit einem Tippfehler
from unittest.mock import MagicMock
mock_notifier = MagicMock()
# Ups, ein Tippfehler! Die echte Methode ist send_message
mock_notifier.send_mesage("hello") # Es wird kein Fehler ausgelöst!
mock_notifier.send_mesage.assert_called_with("hello") # Diese Assertion besteht!
# Unser Test ist grün, aber der Produktionscode würde fehlschlagen.
Um dies zu verhindern, bietet `unittest.mock` die Argumente `spec` und `autospec`.
- `spec=SomeClass`: Dies konfiguriert den Mock so, dass er die gleiche API wie `SomeClass` hat. Wenn Sie versuchen, auf eine Methode oder ein Attribut zuzugreifen, das auf der echten Klasse nicht existiert, wird ein `AttributeError` ausgelöst.
- `autospec=True` (oder `autospec=SomeClass`): Dies ist noch leistungsfähiger. Es verhält sich wie `spec`, überprüft aber auch die Aufrufsignatur aller gemockten Methoden. Wenn Sie eine Methode mit der falschen Anzahl oder den falschen Namen von Argumenten aufrufen, wird ein `TypeError` ausgelöst, genau wie es das echte Objekt tun würde.
from unittest.mock import create_autospec
# Erstellen Sie einen Mock, der die gleiche Schnittstelle wie unsere Notifier-Klasse hat
spec_notifier = create_autospec(Notifier)
try:
# Dies wird wegen des Tippfehlers sofort fehlschlagen
spec_notifier.send_mesage("hello")
except AttributeError as e:
print(e) # Ausgabe: Mock object has no attribute 'send_mesage'
try:
# Dies wird fehlschlagen, weil die Signatur falsch ist (kein 'text'-Keyword)
spec_notifier.send_message("hello")
except TypeError as e:
print(e) # Ausgabe: missing a required argument: 'text'
# Dies ist die korrekte Art, es aufzurufen
spec_notifier.send_message(text="hello") # Das funktioniert!
spec_notifier.send_message.assert_called_once_with(text="hello")
Best Practice: Verwenden Sie beim Patchen immer `autospec=True`. Es macht Ihre Tests robuster und weniger brüchig. `@patch('path.to.thing', autospec=True)`.
Praxisbeispiel: Testen eines Datenverarbeitungsdienstes
Lassen Sie uns alles mit einem vollständigeren Beispiel zusammenfassen. Wir haben einen `ReportGenerator`, der von einer Datenbank und einem Dateisystem abhängt.
# in Datei: app/services.py
class DatabaseConnector:
def get_sales_data(self, start_date, end_date):
# In der Realität würde dies eine Datenbank abfragen
raise NotImplementedError("This should not be called in tests")
class FileSaver:
def save_report(self, path, content):
# In der Realität würde dies in eine Datei schreiben
raise NotImplementedError("This should not be called in tests")
# in Datei: app/reports.py
from .services import DatabaseConnector, FileSaver
class ReportGenerator:
def __init__(self):
self.db_connector = DatabaseConnector()
self.file_saver = FileSaver()
def generate_sales_report(self, start_date, end_date, output_path):
"""Ruft Verkaufsdaten ab und speichert einen formatierten Bericht."""
raw_data = self.db_connector.get_sales_data(start_date, end_date)
if not raw_data:
report_content = "No sales data for this period."
else:
total_sales = sum(item['amount'] for item in raw_data)
report_content = f"Total Sales from {start_date} to {end_date}: ${total_sales:.2f}"
self.file_saver.save_report(path=output_path, content=report_content)
return True
Schreiben wir nun einen Unit-Test für `ReportGenerator.generate_sales_report`, der seine Abhängigkeiten mockt.
# in Datei: tests/test_reports.py
import unittest
from datetime import date
from unittest.mock import patch, Mock
from app.reports import ReportGenerator
class TestReportGenerator(unittest.TestCase):
@patch('app.reports.FileSaver', autospec=True)
@patch('app.reports.DatabaseConnector', autospec=True)
def test_generate_sales_report_with_data(self, mock_db_connector_class, mock_file_saver_class):
"""Testet die Berichterstellung, wenn die Datenbank Daten zurückgibt."""
# Arrange: Richten Sie unsere Mocks ein
mock_db_instance = mock_db_connector_class.return_value
mock_file_saver_instance = mock_file_saver_class.return_value
# Konfigurieren Sie den Datenbank-Mock, um einige gefälschte Daten zurückzugeben (Stub)
fake_data = [
{'id': 1, 'amount': 100.50},
{'id': 2, 'amount': 75.00},
{'id': 3, 'amount': 25.25}
]
mock_db_instance.get_sales_data.return_value = fake_data
start = date(2023, 1, 1)
end = date(2023, 1, 31)
path = '/reports/sales_jan_2023.txt'
# Act: Erstellen Sie eine Instanz unserer Klasse und rufen Sie die Methode auf
generator = ReportGenerator()
result = generator.generate_sales_report(start, end, path)
# Assert: Überprüfen Sie die Interaktionen und Ergebnisse
# 1. Wurde die Datenbank korrekt aufgerufen?
mock_db_instance.get_sales_data.assert_called_once_with(start, end)
# 2. Wurde der Dateispeicher mit dem korrekten, berechneten Inhalt aufgerufen?
expected_content = "Total Sales from 2023-01-01 to 2023-01-31: $200.75"
mock_file_saver_instance.save_report.assert_called_once_with(
path=path,
content=expected_content
)
# 3. Hat unsere Methode den korrekten Wert zurückgegeben?
self.assertTrue(result)
Dieser Test isoliert die Logik innerhalb von `generate_sales_report` perfekt von den Komplexitäten der Datenbank und des Dateisystems, während er dennoch überprüft, dass sie korrekt mit ihnen interagiert.
Best Practices für effektives Mocking
- Halten Sie Mocks einfach: Ein Test, der eine sehr komplexe Mock-Konfiguration erfordert, ist oft ein Anzeichen (ein „Test Smell“) dafür, dass die zu testende Einheit zu komplex ist und möglicherweise das Single-Responsibility-Prinzip verletzt. Erwägen Sie ein Refactoring des Produktionscodes.
- Mocken Sie Kollaborateure, nicht alles: Sie sollten nur Objekte mocken, mit denen Ihre zu testende Einheit kommuniziert (ihre Kollaborateure). Mocken Sie nicht das Objekt, das Sie selbst testen.
- Bevorzugen Sie `autospec=True`: Wie erwähnt, macht dies Ihre Tests robuster, indem sichergestellt wird, dass die Schnittstelle des Mocks mit der des realen Objekts übereinstimmt. Dies hilft, Probleme zu erkennen, die durch Refactoring verursacht werden.
- Ein Mock pro Test (idealerweise): Ein guter Unit-Test konzentriert sich auf ein einzelnes Verhalten oder eine Interaktion. Wenn Sie feststellen, dass Sie viele verschiedene Objekte in einem Test mocken, ist es möglicherweise besser, ihn in mehrere, fokussiertere Tests aufzuteilen.
- Seien Sie spezifisch in Ihren Assertions: Überprüfen Sie nicht nur `mock.method.assert_called()`. Verwenden Sie `assert_called_with(...)`, um sicherzustellen, dass die Interaktion mit den korrekten Daten stattgefunden hat. Dies macht Ihre Tests wertvoller.
- Ihre Tests sind Dokumentation: Verwenden Sie klare und beschreibende Namen für Ihre Tests und Mock-Objekte (z. B. `mock_api_client`, `test_login_fails_on_network_error`). Dies macht den Zweck des Tests für andere Entwickler klar.
Fazit
Test-Doubles sind nicht nur ein Werkzeug zum Testen; sie sind ein grundlegender Bestandteil des Entwurfs testbarer, modularer und wartbarer Software. Indem Sie echte Abhängigkeiten durch kontrollierte Substitute ersetzen, können Sie eine Test-Suite erstellen, die schnell, zuverlässig und in der Lage ist, jede Ecke der Logik Ihrer Anwendung zu überprüfen.
Pythons unittest.mock-Bibliothek bietet ein erstklassiges Toolkit zur Implementierung dieser Muster. Durch die Beherrschung von `MagicMock`, `patch` und der Sicherheit von `autospec` erschließen Sie sich die Fähigkeit, wirklich isolierte Unit-Tests zu schreiben. Dies befähigt Sie, komplexe Anwendungen mit Zuversicht zu erstellen, in dem Wissen, dass Sie ein Sicherheitsnetz aus präzisen, gezielten Tests haben, um Regressionen abzufangen und neue Funktionen zu validieren. Also legen Sie los, beginnen Sie mit dem Patchen und bauen Sie noch heute robustere Python-Anwendungen.