Sie sind auf Seite 1von 7

Zustand als Abhängigkeit -

IoC konsequent gedacht


Ralf Westphal
Design for Testability ist auch Design for Flexibility. Deshalb liebe ich TDD. Allerdings bin ich
dabei immer wieder über einen Problemfall gestolpert: die Prüfung des Zustands eines system under
test (SUT). Als Beispiel hier ein typischer (nicht unbedingt optimaler) Ansatz bei der bekannten
und beliebten KataPotter.
Viele denken bei der Implementierung der Preisberechnung für die KataPotter an einen Warenkorb,
den es zu füllen gilt, um dann den Preis unter Anwendung der Rabatte zu berechnen. Sie definieren
eine solche Klasse dann z.B. so:
class KataPotterShoppingBasket
{
private Dictionary<int, int> books = new Dictionary<int, int>();

public void AddBook(int bookId)


{
//...
}

public decimal Total


{
get
{
//...
}
}
}
Ein zugehörige Test scheint zunächst einfach:
[TestFixture]
public class testKataPotterShoppingBasket
{
[Test]
public void Enthält_ein_Buch()
{
var sut = new KataPotterShoppingBasket();

sut.AddBook(1);

//?
}
}
Aber, ach, ohje… Wie prüft man denn nun, ob das hinzugefügte Buch auch im Warenkorb
angekommen ist? AddBook() ist ein Kommando und verändert den Zustand des Warenkorbs. Wie
kann man also ganz allgemein diesen Zustand in Tests prüfen?

Zugriffsroutinen für Zustand?


Naheliegend ist die Prüfung von Zustand durch direkten Zugriff auf den Zustand. Dafür muss das
SUT nur einen solchen Zugriff anbieten. Für einen üblichen Warenkorb mag das der Fall sein. Da
Zustand als Abhängigkeit, 26.12.2009 Seite 1
gibt es nicht nur ein AddBook(), sondern sicher auch noch ein GetBook() oder eine ganze
Collection von Books, über die man prüfen kann, ob eine Zustandsveränderung wirksam war.
Der KataPotter-Warenkorb braucht solche Zugriffsroutinen aber nicht. Warum sollten sie also nur
für den Test eingeführt werden? Ich halte nichts davon, weil sie das Interface des system under test
aufblähen. Sie haben keinen Wert für die Domäne, sind in der Intellisense-Liste der Eigenschaften
und Methoden aber immer sichtbar. Da hilft auch die Deklaration als intern nicht viel. Das wäre
falsch verstandenes Design for Testability.
Wenn Sie die KataPotter so wie oben gezeigt angehen sollten, widerstehen Sie der Versuchung,
dem Warenkorb z.B. eine Count-Eigenschaft zu geben, um den Test zum Laufen zu bekommen.
Solch ein Design wäre dann zwar irgendwie testbarer, aber die Flexibilität würde sich nicht
erhöhen. Sie hätten nicht mehr das minimal Notwendige getan. Außerdem würde der Test dann
zweierlei testen. Das ist zu vermeiden! Er würde AddBook() und Count prüfen. In beiden könnten
ja Fehler stecken.
AddBook() und Count wären sogar gegenseitig abhängig in Bezug auf Tests. Sie könnten auch
Count nicht zuerst testen, denn dazu müssten Sie Zustand aufbauen. Wäre das nicht der Fall, ist die
Nutzung von mehreren SUT-Funktionalitäten in einem Test nicht so schlimm. Dann können Sie die
in separaten Tests vorweg prüfen.
Ergebnis: Vermeiden Sie, spezielle Zugriffsmethoden für Zustand nur zum Zweck des
Testens einzurichten. Allemal, wenn sie sie nicht wirklich isoliert testen können.

Zustandstest durch Ableitung?


Auf Zustand können Sie aber nicht nur von außen zugreifen, einfacher ist es von innen. Eine
Alternative zu Zugriffsmethoden wäre daher die Ableitung einer Klasse vom SUT. Deren Zustand
wäre ja der des Warenkorbs. Dazu müssten Sie nur die Sichtbarkeit des Zustands ändern:
class KataPotterShoppingBasket
{
protected Dictionary<int, int> books = new Dictionary<int, int>();
...
}
Eine Ableitung kann dann “gefahrlos” eine Zugriffsmethode anbieten. Die “verunreinigt” die
Domänenklasse nicht:
class TestableShoppingBasket : KataPotterShoppingBasket
{
public int Count { get { return base.books.Count; } }
}

Zustand als Abhängigkeit, 26.12.2009 Seite 2


Wenn Sie im Test die abgeleitete Klasse als SUT instanzieren, haben Sie also Testbarkeit “ohne
Reue”:
[Test]
public void Enthält_ein_Buch()
{
var sut = new TestableShoppingBasket();

sut.AddBook(1);

Assert.That(sut.Count, Is.EqualTo(1));
}
Mit einer Ableitung müssen Sie nicht einmal Design for Testability betreiben. Und die Flexibilität
wird auch nicht eingeschränkt. Eine scheinbar ideale Lösung, oder? Der Eingriff, private Variablen
auf protected statt private zu setzen, ist minimal. Und dass dieser Ansatz an abgeschlossenen
(sealed) Klassen scheitert, ist auch unbedeutend.
Dennoch will mir eine Ableitung vom eigentlichen SUT nicht recht schmecken. Nein, das liegt
nicht daran, dass ich Ableitungen für generell überbewertet halte. Hier wäre ja sogar das Liskov
Substitution Principle eingehalten. Den Aufwand für die Ableitungsklasse finde ich auch nicht zu
hoch.
Nein, es etwas anderes, das mich stört…

Zustand als Abhängigkeit


Ich glaube, mich stört, dass Zustand hier eine Extrawurst gebraten bekommt. Zustand wird nicht
seiner Natur nach behandelt. Denn Zustand ist eine Abhängigkeit!
Üblicherweise denken wir bei Abhängigkeiten an Funktionalität. Der Warenkorb könnte z.B. von
einer Preisberechnungsfunktionalität abhängen:

Zustand als Abhängigkeit, 26.12.2009 Seite 3


Dann fänden wir es ganz natürlich, diese Abhängigkeit in den Warenkorb zu injizieren:
class KataPotterShoppingBasket
{
private IPriceCalculator calc;
private Dictionary<int, int> books = new Dictionary<int, int>();

public KataPotterShoppingBasket(IPriceCalculator calc)


{
this.calc = calc;
}
...
Aber was ist jetzt das private Feld calc? Es gehört genauso zum Zustand des Warenkorbs wie
books. Der einzige Unterschied besteht darin, dass calc von einem selbstdefinierten Typ ist und
book einen .NET Fx Typ hat.
Darüber hinaus glauben wir dann aber irgendwie noch eine größere “Intimität” zwischen book und
z.B. AddBook() zu spüren. Die Inhalte von book werden mittels der Warenkorbmethoden verändert,
während calc konstant bleibt. book ist eben “wahrer” Zustand und deshalb anders zu behandeln als
calc.
Doch warum ist die Abhängigkeit von einem Preisberechner denn offengelegt? Warum wird der
injiziert? Weil das den Warenkorb isoliert testbar macht. Sie können ihn unter Injektion einer
Attrappe auf die Probe stellen. Dazu kommt, dass damit die Preisberechnung auch noch flexibler
austauschbar wird; ein schöner Nebeneffekt.
Wenn nun jedoch bessere Testbarkeit zur IoC für funktionale Abhängigkeiten geführt hat, warum
sollte sie dann nicht auch bei Abhängigkeiten von Zustand zum Einsatz kommen? Ich sehe da kein
gewichtiges Gegenargument. Das widerspräche auch nicht der Objektorientierung. Nach Injektion
ist Zustand ja weiterhin in einem Objekt entsprechend dessen Zugriffsmethoden verborgen.
Wenn wir von Abhängigkeiten sprechen, sollten wir also konsequent sein. Zustand (state) wie
Funktionalität (behavior) sind zwei Seiten derselben Medaille Abhängigkeit:

Zustand als Abhängigkeit, 26.12.2009 Seite 4


Aus meiner Sicht sieht die Lösung des Ausgangsproblems deshalb so aus:
class KataPotterShoppingBasket
{
private readonly PriceCalculator calc;
private readonly Dictionary<int, int> books;

public KataPotterShoppingBasket(Dictionary<int, int> books,


PriceCalculator calc)
{
this.calc = calc;
this.books = books;
}
...
Dem erweiterten Warenkorb injizieren Sie nicht nur die Abhängigkeit von einer
Preisberechnungsfunktionalität, sondern auch einen initialen Zustand:
[Test]
public void Enthält_ein_Buch()
{
var books = new Dictionary<int, int>();
var sut = new KataPotterShoppingBasket(
books,
null);

sut.AddBook(1);

Assert.That(books.Count, Is.EqualTo(1));
}
Das halte ich für genauso konsequent wie einfach zu verstehen. Etwas systematisiert lautet dann das
Muster:
1. Abhängigkeiten einer Klasse zusammenfassen in einer Abhängigkeitsklasse. Das gilt für
Zustände wie Funktionalität, von denen eine Klasse abhängt.
2. Während Tests die dafür minimal nötigen Abhängigkeiten mit einer Instanz der
Abhängigkeitsklasse injizieren.
3. Erwartungen an Zustandsänderungen nach Ausführung von Kommandos auf dem SUT
prüfen anhand der injizierten Abhängigkeitklasseninstanz.

Zustand als Abhängigkeit, 26.12.2009 Seite 5


Am Beispiel des Warenkorbs sähe das so aus:
class KataPotterShoppingBasket
{
internal class Dependencies
{
public readonly IPriceCalculator calc;
public readonly Dictionary<int, int> books = …
}

private readonly Dependencies dependencies;

public KataPotterShoppingBasket(Dependencies dependencies)


{
this.dependencies = dependencies;
}
...
Der Test wäre sogar noch ein wenig klarer, finde ich:
[Test]
public void Enthält_ein_Buch()
{
var dependencies = new KataPotterShoppingBasket.Dependencies();
var sut = new KataPotterShoppingBasket(dependencies);

sut.AddBook(1);

Assert.That(dependencies.books.Count, Is.EqualTo(1));
}
Diesem Verfahren steht auch die “normale” Dependency Injection nicht im Wege. Sie würde es
sogar verbergen. Dazu bräuchte die Abhängigkeitsklasse nur einen ausgewiesenen “Kanal” für die
Injektion der Funktionalität, von der der Warenkorb abhängig ist, z.B. so für Unity als DI
Container:
public class Dependencies
{
[Dependency]
public IPriceCalculator calc {get; set;}
public readonly Dictionary<int, int> books = …
}
Zusätzlich müsste im Mapping auch die Abhängigkeitsklasse aufgeführt werden. Aber das ist kein
sonderlicher Mehraufwand. Das ließe sich womöglich sogar automatisieren.

Zustand als Abhängigkeit, 26.12.2009 Seite 6


Fazit
Im Sinne eines systematischen Vorgehens beim Test und beim Design finde ich es sehr
überlegenswert, Abhängigkeiten breiter als bisher zu fassen. Funktionseinheiten sind nicht nur
von anderen Funktionseinheiten abhängig, sondern auch von Zustand.
Wenn dann Abhängigkeiten injiziert werden sollen, um bessere Testbarkeit zu erreichen, dann gilt
das nicht nur für Funktionseinheiten, sondern auch für Zustand.
Gerade bei TDD sinkt dadurch – so mein Empfinden – der gedankliche Aufwand. Bei jedem
Kommando und jeder Abfrage, die getestet werden, müssen Sie sich dann einfach nur die Frage
stellen, welcher Zustand dabei eine Rolle spielt. Dann bauen Sie vor dem Test Ausgangszustand
speziell für den Testfall zusammen (arrange) und injizieren ihn, rufen das SUT (act) auf und
prüfen anschließend (bei Kommandos) den neuen Zustand (assert). Fertig.
Ich werde in der nächsten Zeit mal probeweise konsequent so vorgehen. Mal schauen, wie sich das
anfühlt. Ich bin optimistisch, dass es ein gutes Gefühl sein wird. Weniger Zweifel, weniger
Nachdenken, also höhere Produktivität.

Zustand als Abhängigkeit, 26.12.2009 Seite 7

Das könnte Ihnen auch gefallen