Training - Beratung - Projektarbeiten

www.David-Tielke.de

Test Driven Development mit .NET und C#–Tipp #2: Abhängigkeiten auflösen mit Stubs

Das Testen von Code ist eine wichtige und – für Entwickler – richtig coole Angelegenheit. Doch Testen ist weitaus mehr, als ein Testrunner und ein paar Attribute für Klassen und Methoden. In der Reihe “TDD Tipps” versuche ich regelmäßig Probleme zu erläutern, die beim Testen auftauchen und mit einem praktischen Hinweis einen Lösungsweg aufzuzeigen.

Unit-Tests sollen das Verhalten von nur einem einzigen Modul, für uns in diesem Fall einer Klasse, testen. Aber warum? Nehmen wir an wir testen die Methode Foo() in einer Klasse A die eine Referenz auf eine Klasse B hat. Im Laufe der Methode Foo() aus Klasse A wird die Methode Bar() aus Klasse B aufgerufen. Wenn wir jetzt A.Foo() testen, wird Code aus B.Bar() ausgeführt. Also ist das Ergebnis unseres Tests nicht mehr alleine von A, sondern auch von B abhängig und damit hängt auch der Testerfolg von A.Foo() von B.Bar() ab. Was hier noch nach einem einfachen Problem aussieht, wird spätestens zu einem Problem, wenn B eine externe Ressource nutzen würde, wie einen Webdienst, einen Mailserver, Datenbankserver o.ä. Aus dem und vielen anderen Gründen, sollte man in seinem Design immer so wenig Abhängigkeiten wie möglich haben. Was in der Theorie und in den zahlreichen Demos in Büchern und Konferenzen immer ganz einfach und simpel aussieht, gestaltet sich in der Praxis als teilweise sehr kompliziert. Wenn man ein Klassendesign entwirft, hat man an einigen Stellen zwangsläufig Abhängigkeiten.

Abbildung 1 verdeutlich das Problem noch einmal:

image

Abbildung 1: Klassendesign das getestet werden soll.

Würden wir nun einen Test für die Methode Foo() ) der Klasse A schreiben und dort ein Objekt der Klasse A instanziieren, würde dieses automatisch ein Objekt der Klasse B erzeugen, da A.Foo() ja deren Funktionalität nutzt.

Um solche Abhängigkeiten aufzulösen, bietet sich der Einsatz von Interfaces an. Mit nur einem Interface haben wir die Klassen voneinander entkoppelt und die Abhängigkeit zwischen A und B aufgelöst.image

Abbildung 2: Die Klassen A und B sind durch das Interface entkoppelt.

Zusätzlich übernimmt jetzt nicht mehr A selbst die Instanziierung von B, sondern wir injizieren dem Objekt von A mittels Konstruktorparameter b ein Objekt, das die Schnittstelle IB implementiert. Dadurch können wir von außen bestimmen, welches Objekt die Funktionalität IB.Bar() übernimmt, die wir von A.Foo() aus aufrufen.

Man stelle sich vor ,dass B.Bar() eine Datenbank aufruft. Gibt es dort einen Fehler, löst diese Methode eine InvalidOperationException aus. Unsere Methode A.Foo(), die ja B.Bar() aufruft, soll auf diese Exception mit einer CustomException reagieren. Wenn wir das mit dem Design aus Abbildung 1 testen möchten, müssten wir eine Datenbank aufsetzen, diese Tabellen in der Datenbank so manipulieren, dass in der Methode B.Bar() die gewünschte Exception auftritt und dann den Test für A.Foo() durchführen. Wir müssen quasi ein Szenario für B.Bar() simulieren, obwohl wir nur A.Foo() testen wollen. Das ist hier bereits verwirrend und in größeren Systemen führt das zu unglaublich kompliziertem Testcode den nach ein paar Tagen der Tester selbst nicht mehr versteht.

Mit dem Design aus Abbildung 2 ist das überhaupt kein Problem: In unserem Testprojekt legen wir einen sogenannten Stub an. Stub heißt, das er ein “dummer” Stellvertreter einer Interfaceimplementierung ist, der fest definierte Werte zurückgibt.

public class IBExceptionStub : IB
{
    public void Bar()
    {
        throw new InvalidOperationException("...");
    }
}

Listing 1: Ein Stub für einen Test der von B.Bar() eine Exception erwartet.

Schreiben wir nun also einen Test, der prüfen möchte, ob A.Foo() die erwartete CustomException auch tatsächlich auslöst, brauchen wir nur noch folgenden Code zu schreiben:

[TestMethod]
[ExpectedException(typeof(CustomException))]
public void Foo_DatabaseNotAvailable_CustomException()
{
    IB ib = new IBExceptionStub();
    A a = new A(ib);

    a.Foo();
}

Listing 2: Der Test für A.Foo() nutzt den ExceptionStub um einen Fehlerfall von B.Bar() zu simulieren.

Damit haben wir zwei Punkte erreicht:

Erstens können wir unterschiedlichste Verhalten von A in Abhängigkeit von Verhalten in B testen. Möchten wir z.B. ein Verhalten von A.Foo testen, wenn von B.Bar() aus der Datenbank ein ungültiger User geladen wird, schreiben wir einfach einen kleinen Stub.

Zweitens das Design ist wesentlich flexibler geworden durch die Entkopplung durch das Interface. Das Design aus Abbildung 1 wäre nicht durch eine Klasse C ersetzbar, welche die Methode Bar() ebenfalls anbietet. Das zweite Design könnte dies ohne Probleme, wenn C das Interface IB implementieren würde.

Das Ganze funktioniert so natürlich nur wenn Constructorinjection möglich ist. Sollte das nicht möglich sein, nutzt man dazu das ServiceLocator-Pattern. Aber das schauen wir uns in einem anderen Tipp genauer an.

Alle Beiträge der Reihe “TDD Tipps” sind hier zu finden.

Kommentar schreiben

Loading