Mit monkeypatch
können Funktionen und Klassen während des Tests simuliert werden.
Dadurch können einzelne Programmteile isoliert getestet werden.
Ein Unittest soll eine einzelne Funktion oder sogar nur Teile einer Funktion testen. Die zu testende Funktion wird aber in der Regel Objekte verarbeiten und weitere Funktionen/Methoden aufrufen. Funktionen und Klassen werden bei Unittests simuliert (oder „gemockt“), um bestimmte Abhängigkeiten zu isolieren und das Testen einzelner Komponenten zu ermöglichen, ohne dass externe Systeme oder komplexe Abläufe involviert sind. Hier sind die Hauptgründe, warum Mocking beim Testen hilfreich ist:
Mocking hilft, sich nur auf die zu testende Funktion oder Klasse zu konzentrieren, ohne dass Abhängigkeiten zu anderen Modulen oder Komponenten den Test beeinflussen. So können wir sicherstellen, dass der Test nur das Verhalten der spezifischen Komponente überprüft und nicht durch externe Faktoren verzerrt wird.
In vielen Anwendungen greifen Funktionen und Klassen auf externe Ressourcen zu, wie Datenbanken, APIs, Dateisysteme oder Netzwerke. Diese Ressourcen sind oft schwer zugänglich, langsam oder unzuverlässig. Durch Mocking werden solche Abhängigkeiten ersetzt, sodass Tests schnell, zuverlässig und unabhängig von der Verfügbarkeit externer Ressourcen sind.
Mocks ermöglichen es, gezielt verschiedene Rückgabewerte oder Fehler zu simulieren, um zu überprüfen, wie die getestete Komponente darauf reagiert. So lassen sich Szenarien testen, die in der realen Umgebung schwer reproduzierbar wären, wie Netzwerkfehler oder spezifische Ausnahmefälle.
Da Mocks die tatsächlichen Ausführungen komplexer Funktionen oder Klassen ersetzen, laufen Tests oft schneller und benötigen weniger Ressourcen. Besonders bei großen Test-Suites führt dies zu einer deutlichen Zeitersparnis.
Insgesamt macht Mocking Unittests flexibler, effizienter und zuverlässiger, was die Qualität und Wartbarkeit des Codes verbessert.
Nehmen wir einmal an, wir möchten eine Funktion process_data
testen.
Diese Funktion ruft eine andere Funktion get_data_from_database
auf, welche die Daten liest..
Für unsere Unittests der Funktion process_data
wollen wir die Funktion get_data_from_database
simulieren.
# my_module.py def get_data_from_database(): """ Diese Funktion würde normalerweise eine Datenbank abfragen und Daten zurückgeben. Wir simulieren diese Funktion im Test. """ # Hier könnte normalerweise eine Datenbankabfrage stehen raise NotImplementedError("Diese Funktion greift normalerweise auf eine Datenbank zu.") def process_data(): """ Diese Funktion ruft Daten von der Datenbank ab und verarbeitet sie. """ data = get_data_from_database() # Verarbeiten wir die Daten (hier nur ein einfaches Beispiel) return [item * 2 for item in data]
import pytest from my_module import process_data # Fixture zum Simulieren der Datenbankabfrage-Funktion @pytest.fixture def mock_get_data_from_database(monkeypatch): """ Simuliert die Funktion `get_data_from_database`, um feste Testdaten zurückzugeben. """ def mock_data(): return [1, 2, 3] # Beispielhafte Testdaten, die anstelle echter Daten zurückgegeben werden # Ersetzen der echten Funktion durch die simulierte Version monkeypatch.setattr("my_module.get_data_from_database", mock_data) def test_process_data(mock_get_data_from_database): """ Testet die Funktion `process_data`, indem die Datenbankabfrage-Funktion simuliert wird. """ result = process_data() assert result == [2, 4, 6], "Die Daten sollten verdoppelt werden."
Mit der Zeile monkeypatch.setattr(„my_module.get_data_from_database“, mock_data)
teilen wir Pytest mit,
dass anstelle von get_data_from_database
die Funktion mock_data
aufgerufen wird.
Auch ganze Klassen mit ihren Attributen und Methoden lassen sich in Unittests simulieren. Einerseits lässt sich dadurch eine Funktion testen, bevor die Klasse überhaupt realisiert wurde. Andererseits isolieren wir die zu testende Funktion von allfälligen Fehlern in der Umsetzung der Klasse.
# database_module.py class DatabaseClient: """ Klasse zum Verbinden mit einer Datenbank und Abrufen von Daten. """ def fetch_data(self): # In einer echten Anwendung würde hier eine Datenbankabfrage stehen. raise NotImplementedError("Verbindung zur echten Datenbank.") def get_processed_data(db_client): """ Funktion, die Daten vom DatabaseClient abruft und verarbeitet. """ data = db_client.fetch_data() # Beispielhafte Verarbeitung: jedes Element um 1 erhöhen return [item + 1 for item in data]
# test_database_module.py import pytest from database_module import get_processed_data # Fixture zum Simulieren der Klasse DatabaseClient @pytest.fixture def mock_db_client(monkeypatch): """ Simuliert die DatabaseClient-Klasse, um feste Testdaten zurückzugeben. """ # Simulierte Klasse class MockDatabaseClient: def fetch_data(self): return [10, 20, 30] # Beispielhafte Testdaten # Ersetzen der echten DatabaseClient-Klasse durch die simulierte Version monkeypatch.setattr("database_module.DatabaseClient", MockDatabaseClient) # Instanz der simulierten Klasse zurückgeben return MockDatabaseClient() def test_get_processed_data(mock_db_client): """ Testet die Funktion `get_processed_data`, indem die DatabaseClient-Klasse simuliert wird. """ result = get_processed_data(mock_db_client) assert result == [11, 21, 31], "Die Daten sollten um 1 erhöht werden."
Fixture mock_db_client:
Diese Fixture erstellt eine simulierte Version der DatabaseClient
-Klasse, die eine Methode fetch_data()
bereitstellt, welche die Beispiel-Daten [10, 20, 30] zurückgibt.
Mit monkeypatch.setattr
ersetzen wir die echte DatabaseClient-Klasse in database_module
durch die simulierte MockDatabaseClient-Klasse. Die Fixture gibt eine Instanz von MockDatabaseClient
zurück, die im Test verwendet wird.
Test test_get_processed_data:
Der Test nutzt die Fixture mock_db_client
, um get_processed_data
zu testen.
Da get_processed_data
nun auf die simulierten Daten [10, 20, 30] zugreift, sollte das Ergebnis [11, 21, 31] sein (jedes Element um 1 erhöht). Der Test überprüft dies mit einer assert
-Anweisung.