LU04b - Funktionen und Klassen simulieren

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:

Isolation der Testobjekte

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.

Vermeidung externer Abhängigkeiten

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.

Kontrolle über Rückgabewerte und Fehlerzustände

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.

Verbesserte Performance

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.

Funktionen simulieren

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.

Klassen simulieren

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."

Erläuterung

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.