Mock Object Pattern

Mock Object Pattern

vg

Das Mock Object Pattern ist ein Designmuster, das vor allem im Bereich des Testens von Software verwendet wird. Es hilft dabei, Abhängigkeiten von realen Objekten durch speziell erstellte, simulierte Objekte (Mocks) zu ersetzen. Mock-Objekte ermöglichen es, Teile eines Systems isoliert zu testen, ohne dass die gesamten Abhängigkeiten vorhanden sein müssen.

Was ist ein Mock Object?

Ein Mock-Objekt ist eine Nachbildung eines echten Objekts, das in einem Test verwendet wird. Es verhält sich wie das reale Objekt, jedoch werden nur spezifische Funktionen nachgebildet, die im Test benötigt werden. Diese Mocks sind besonders nützlich, wenn echte Objekte schwer zugänglich sind, z.B. Datenbankverbindungen oder Webservices. Sie erlauben es, das Verhalten eines Systems zu überprüfen, ohne auf externe Systeme angewiesen zu sein.

Warum Mock-Objekte verwenden?

Mock-Objekte bieten zahlreiche Vorteile:

  • Isolierte Tests: Sie ermöglichen das Testen einzelner Komponenten, ohne die gesamte Infrastruktur zu benötigen.
  • Unabhängigkeit: Man kann die getesteten Klassen von externen Abhängigkeiten trennen.
  • Schnellere Tests: Der Test läuft schneller, da echte Ressourcen wie Datenbanken oder Netzwerke nicht aufgerufen werden.
  • Erhöhte Flexibilität: Man kann verschiedene Szenarien simulieren, die mit realen Objekten schwierig oder unmöglich wären.

Mock Objects im Vergleich zu Stubs und Spies

Mock-Objekte sind häufig mit Stubs und Spies verwandt, jedoch gibt es Unterschiede:

  • Stubs: Stellen ein einfaches Ersatzobjekt dar, das festgelegte Werte zurückgibt, um den Testablauf zu ermöglichen.
  • Spies: Beobachten und protokollieren die Interaktionen mit dem Objekt, ohne sein Verhalten zu ändern.
  • Mocks: Mocks sind komplexer. Sie überprüfen, ob bestimmte Methoden in einer bestimmten Reihenfolge oder Anzahl aufgerufen wurden.

Beispiel des Mock Object Pattern in C++

In C++ kann das Mock Object Pattern manuell umgesetzt werden, um Tests zu vereinfachen. Nehmen wir an, wir haben eine Database-Klasse, die mit einer externen Datenbank kommuniziert. Für Tests möchten wir jedoch keine echte Datenbankverbindung herstellen, sondern ein Mock-Objekt verwenden.

Schritt 1: Definieren der Schnittstelle

Zuerst definieren wir eine Schnittstelle, die unsere Database-Klasse implementieren wird.

class IDatabase {
public:
    virtual ~IDatabase() {}
    virtual void connect() = 0;
    virtual void query(const std::string& sql) = 0;
};

Schritt 2: Implementierung der echten Database-Klasse

Nun implementieren wir die echte Database-Klasse, die die IDatabase-Schnittstelle verwendet. Diese Klasse stellt die eigentliche Verbindung zu einer Datenbank her.

class Database : public IDatabase {
public:
    void connect() override {
        std::cout << "Verbindung zur echten Datenbank hergestellt." << std::endl;
    }

    void query(const std::string& sql) override {
        std::cout << "Abfrage ausgeführt: " << sql << std::endl;
    }
};

Schritt 3: Erstellen eines Mock-Objekts

Nun erstellen wir ein Mock-Objekt, das die gleiche Schnittstelle implementiert. Das Mock-Objekt ersetzt die echte Database-Klasse in unseren Tests.

class MockDatabase : public IDatabase {
public:
    void connect() override {
        std::cout << "Verbindung zur Mock-Datenbank hergestellt." << std::endl;
    }

    void query(const std::string& sql) override {
        std::cout << "Mock-Abfrage ausgeführt: " << sql << std::endl;
    }
};

Schritt 4: Testen mit dem Mock-Objekt

Nun verwenden wir das Mock-Objekt, um die Database-Klasse zu testen, ohne eine echte Datenbankverbindung herzustellen.

void testDatabaseConnection(IDatabase& db) {
    db.connect();  // Testet die Verbindungslogik
    db.query("SELECT * FROM users");  // Testet die Abfragefunktion
}

int main() {
    MockDatabase mockDb;  // Erstelle ein Mock-Objekt
    testDatabaseConnection(mockDb);  // Verwende das Mock-Objekt im Test

    return 0;
}

Erklärung des Codes

  • IDatabase: Diese Schnittstelle stellt sicher, dass alle Klassen, die sie implementieren, die gleichen Methoden zur Verbindung und Abfrage der Datenbank anbieten.
  • Database: Diese Klasse stellt die tatsächliche Datenbankverbindung her und führt Abfragen aus.
  • MockDatabase: Diese Klasse simuliert das Verhalten der Database, aber ohne eine echte Verbindung zur Datenbank herzustellen. Sie wird in Tests verwendet, um das Verhalten zu überprüfen.
  • Testmethode: In der testDatabaseConnection-Funktion wird das Mock-Objekt verwendet, um die Verbindungs- und Abfragelogik zu testen. Es wird sichergestellt, dass der Test keine tatsächliche Datenbankverbindung benötigt.

Beispiel des Mock Object Pattern in Python

Das Mock Object Pattern wird verwendet, um Objekte zu simulieren, die in Tests verwendet werden, ohne dass eine echte Implementierung erforderlich ist. Dies ist besonders nützlich, um Abhängigkeiten zu isolieren und zu kontrollieren, was in einem Test passiert.

Hier ist ein einfaches Beispiel des Mock Object Patterns in Python unter Verwendung des unittest.mock Moduls:

Beispiel:

Angenommen, wir haben eine Klasse, die eine API abfragt, und wir möchten diese API in unseren Tests simulieren, ohne die echte API zu verwenden.

import unittest
from unittest.mock import Mock

# Eine einfache Klasse, die eine API abfragt
class APIClient:
    def get_data(self):
        # Stellt eine Anfrage an eine API und gibt die Antwort zurück
        pass

    def process_data(self):
        data = self.get_data()
        # Verarbeitung der empfangenen Daten
        return f"Verarbeitet: {data}"

# Der Testfall
class TestAPIClient(unittest.TestCase):

    def test_process_data(self):
        # Erstelle ein Mock-Objekt für die APIClient-Klasse
        mock_api_client = Mock(spec=APIClient)
        
        # Definiere, was das Mock-Objekt zurückgeben soll
        mock_api_client.get_data.return_value = "Testdaten"

        # Teste die Methode `process_data` unter Verwendung des Mock-Objekts
        result = mock_api_client.process_data()
        
        # Überprüfen, ob das Mock-Objekt die erwartete Methode aufgerufen hat
        mock_api_client.get_data.assert_called_once()

        # Überprüfen, ob die Verarbeitung korrekt funktioniert
        self.assertEqual(result, "Verarbeitet: Testdaten")

if __name__ == '__main__':
    unittest.main()

Erklärung:

  1. APIClient: Eine Klasse, die eine Methode get_data() hat, die die Daten von einer API abruft, und eine Methode process_data(), die diese Daten verarbeitet.
  2. Mock-Objekt: Im Test erstellen wir ein Mock-Objekt (mock_api_client), das das Verhalten der echten APIClient-Klasse imitiert. Wir definieren, dass die Methode get_data() einen vordefinierten Wert („Testdaten“) zurückgibt.
  3. Test: Im Test überprüfen wir, ob die Methode get_data() tatsächlich aufgerufen wurde und ob die Methode process_data() die erwartete Ausgabe liefert.

Dieses Beispiel zeigt, wie Mock-Objekte verwendet werden können, um externe Abhängigkeiten zu simulieren und fokussiert auf das Testen der Logik innerhalb der zu testenden Methode.

Vorteile des Mock Object Patterns

  1. Isoliertes Testen: Mit Mocks können wir die zu testenden Komponenten isolieren und ihre Interaktion mit externen Systemen simulieren.
  2. Kontrollierte Testszenarien: Mocks ermöglichen es, gezielt Szenarien zu testen, ohne von der Verfügbarkeit und dem Verhalten externer Systeme abhängig zu sein.
  3. Schnellere Ausführung: Tests, die Mock-Objekte verwenden, laufen schneller, da keine realen Ressourcen benötigt werden.

Nachteile des Mock Object Patterns

  1. Komplexität des Testcodes: Der Einsatz von Mock-Objekten kann den Testcode komplexer machen. Wenn viele Mock-Objekte erforderlich sind, muss die Logik zum Erstellen und Verwalten dieser Objekte genau durchdacht werden. Die Erstellung von Mocks kann zusätzliche Zeit in Anspruch nehmen und den Code weniger übersichtlich machen, besonders wenn viele Abhängigkeiten simuliert werden müssen.
  2. Wartung des Testcodes: Mock-Objekte erfordern häufige Aktualisierungen des Testcodes, wenn sich die Implementierung der getesteten Klassen ändert. Wenn die Schnittstellen von Klassen oder Methoden geändert werden, müssen auch die Mocks angepasst werden. Dies kann dazu führen, dass Tests regelmäßig gewartet werden müssen, um mit der realen Implementierung synchron zu bleiben.
  3. Gefahr von falschen Annahmen: Mock-Objekte simulieren nur das Verhalten der echten Objekte und sind nicht immer zu 100 % exakt. Es besteht die Gefahr, dass die Tests fälschlicherweise davon ausgehen, dass das Mock-Objekt das tatsächliche Verhalten der realen Objekte korrekt widerspiegelt. Dies kann dazu führen, dass der Test den tatsächlichen Betrieb in der Produktionsumgebung nicht korrekt widerspiegelt, was die Zuverlässigkeit der Tests beeinträchtigen kann.
  4. Versteckte Fehler: Mock-Objekte testen nur das Verhalten in einem sehr begrenzten Kontext und decken oft nicht alle Interaktionen oder Randfälle ab. In komplexen Systemen können wichtige Fehler oder unerwartete Verhaltensweisen übersehen werden, da Mock-Objekte nicht immer alle Details und Fehlerquellen der echten Objekte simulieren. Infolgedessen kann der Testlauf erfolgreich sein, obwohl der echte Code später Fehler aufweist.
  5. Performance-Probleme bei zu vielen Mocks: In umfangreichen Tests, die viele Mocks und komplexe Abhängigkeiten verwenden, kann die Testausführung selbst beeinträchtigt werden. Wenn zu viele Mocks in einem Test erstellt werden müssen, kann dies die Performance negativ beeinflussen und die Ausführungszeit der Tests erhöhen.

Wann sollte man ein Mock Object Pattern einsetzen?

Das Mock Object Pattern wird hauptsächlich eingesetzt, wenn du das Verhalten von Objekten simulieren möchtest, um eine bestimmte Funktionalität in Tests zu überprüfen, ohne dass du auf echte Implementierungen zugreifen musst. Es hilft dabei, Abhängigkeiten zu isolieren und den Testumfang auf die zu testende Logik zu fokussieren. Hier sind einige typische Szenarien, in denen du das Mock Object Pattern verwenden solltest:

1. Externe Abhängigkeiten simulieren

Wenn dein Code von externen Systemen oder Diensten abhängt, wie etwa Datenbanken, APIs, Webdiensten oder Dateisystemen, möchtest du in deinen Tests oft nicht auf diese externen Ressourcen zugreifen (z.B. wegen Performance-Problemen oder Kosten). In diesem Fall kannst du ein Mock-Objekt verwenden, um diese externen Ressourcen zu simulieren.

Beispiel:

  • Deine Anwendung kommuniziert mit einer externen API. In Tests möchtest du jedoch nicht die echte API aufrufen, sondern ein Mock-Objekt verwenden, das das Verhalten der API nachahmt.

2. Komplexe oder schwer zu testende Abhängigkeiten

Wenn deine Abhängigkeiten schwer zu steuern oder komplex sind (z.B. wenn sie Zustände ändern oder interaktive Benutzeroberflächen erfordern), kann es sinnvoll sein, Mock-Objekte zu verwenden, um die Komplexität zu reduzieren und sich auf die zu testende Logik zu konzentrieren.

Beispiel:

  • Deine Methode interagiert mit einer Datenbank. Um die Tests schnell und isoliert durchzuführen, kannst du die Datenbankzugriffe mocken und kontrollieren, welche Daten zurückgegeben werden.

3. Isolierung der getesteten Klasse

Wenn du sicherstellen möchtest, dass nur die getestete Klasse und ihre Logik überprüft werden und nicht die Implementierungen von anderen Klassen, die sie verwenden, dann helfen Mock-Objekte, die zugehörigen Abhängigkeiten zu isolieren.

Beispiel:

  • Eine Klasse, die auf die Ergebnisse einer Berechnung angewiesen ist, aber du möchtest nur die Logik dieser Berechnung testen, ohne dass die Eingabewerte von anderen Klassen stammen.

4. Kontrolle über Rückgabewerte und Verhalten

Mithilfe von Mock-Objekten kannst du exakt steuern, was eine Methode eines gemockten Objekts zurückgibt, und du kannst prüfen, ob eine Methode wie erwartet aufgerufen wird.

Beispiel:

  • In einem Test möchtest du sicherstellen, dass eine Methode eines externen Dienstes unter bestimmten Bedingungen aufgerufen wird. Du kannst ein Mock-Objekt verwenden, um das Verhalten des externen Dienstes zu kontrollieren und die Methode zu überwachen.

5. Vermeidung von Nebenwirkungen

Wenn die getesteten Objekte Nebenwirkungen haben (z.B. das Schreiben in eine Datei oder das Senden einer E-Mail), möchtest du möglicherweise diese Nebenwirkungen im Test verhindern. Ein Mock-Objekt ermöglicht es dir, die Methode zu simulieren, ohne dass tatsächlich eine Datei geschrieben oder eine E-Mail versendet wird.

Beispiel:

  • Deine Methode sendet eine E-Mail. Im Test möchtest du sicherstellen, dass die E-Mail korrekt gesendet wird, aber ohne, dass tatsächlich eine E-Mail verschickt wird. Ein Mock-Objekt kann das Versenden der E-Mail simulieren und nur das Verhalten überprüfen.

6. Verhalten von nicht implementierten oder noch nicht verfügbaren Komponenten testen

Manchmal ist eine Komponente noch nicht fertig oder noch nicht verfügbar, aber du möchtest den Code trotzdem testen. In solchen Fällen kannst du ein Mock-Objekt verwenden, das das Verhalten dieser Komponente nachahmt, sodass du die Tests trotzdem durchführen kannst.

Beispiel:

  • Deine Anwendung ist noch in der Entwicklung, aber du möchtest bereits jetzt Tests für die Logik schreiben, die mit einer externen Bibliothek oder einem Service interagiert. Du kannst Mock-Objekte der entsprechenden Funktionen erstellen und die Tests unabhängig vom Entwicklungsfortschritt durchführen.

Fazit

Das Mock Object Pattern ist ein leistungsfähiges Werkzeug, um Softwarekomponenten isoliert zu testen. In C++ kann es durch die Erstellung von Mock-Klassen, die dieselben Schnittstellen wie die echten Klassen implementieren, einfach realisiert werden. Mocks bieten eine Möglichkeit, die Testabdeckung zu erweitern, die Testzeit zu verkürzen und das Testen von Softwarekomponenten zu vereinfachen. Sie sind besonders nützlich, wenn man mit externen Systemen arbeitet, die schwer zu simulieren oder zu steuern sind.

Zurück zur Pattern-Übersicht: Liste der Design-Pattern

com

Newsletter Anmeldung

Bleiben Sie informiert! Wir informieren Sie über alle neuen Beiträge (max. 1 Mail pro Woche – versprochen)