Jedes Blog, in dem gelegentlich über Tests geschrieben wird und welches etwas auf sich hält, muss mindestens einmal etwas über das Testen von privaten Klassenmembern schreiben. Zugegeben in einem anderen Post habe ich bereits gezeigt wie das geht, nun habe ich aber endlich ein Praxisbeispiel an dem sich sehr gut zeigen lässt wann man evtl. einen anderen, sichereren Weg gehen sollte.

Entwickelt man seinen Code tatsächlich mit TDD, sollte sich die Frage nach dem Test von privaten Membern eigentlich kaum stellen. Dadurch dass nur Code geschrieben wird, der auch durch Tests abgesichert ist sollten sich in jedem Fall genügend Vorbedingungen ergeben um auch jede private Methode irgendwie durch die öffentlichen Schnittstellen zu prüfen.

Nutzt man automatisierte Tests jedoch ohne TDD, in dem man nur sporadisch Tests schreibt, steht man bei Klassen wie der folgenden vor einem Problem. Getestet werden soll die Configuration des IoC Containers in der Klass AppBootstrapper. Dabei handelt es sich um eine Implementierung des Bootstrappers von Caliburn.Micro, bei dem der Container mit Configure konfiguriert wird und mit GetInstance und GetAllInstance Instanzen der registrierten Typen erstellt werden können.

Der Bootstrapper von Caliburn.Micro

Wie man sieht hat AppBootstrapper aufgrund der Vererbung eine ganze Reihe von protected Methoden und die Konfiguration des IoC Containers, sowie das Auflösen von Instanzen, ist nur ein kleiner Teil davon, der ebenfalls protected ist. Der gesamte Ablauf geschieht einzig über die Start Methode, welche außerdem die einzige Methode ist die von außen aufgerufen werden kann. Man kann also im Grunde sagen, dass es fast unmöglich ist auf einfachem Wege die drei vorliegenden Methoden zu prüfen in dem man nur öffentliche Schnittstellen verwendet.

Hat man mein anderes Post gelesen, könnte nun der erste Gedanke sein einen Accessor mit PrivateObject zu erstellen, anschließend Configure aufzurufen und dann mit GetInstance zu prüfen ob auch alle notwendigen Typen korrekt aufgelöst werden können.

[TestMethod]
public void BootStrapper_shall_return_instance_of_WindowManager()
{
  var bootstrapper = new AppBootstrapper();
  var sut = new PrivateObject(bootstrapper);
  sut.Invoke("Configure");

  var result = sut.Invoke("GetInstance", typeof(IWindowManager));

  Assert.IsNotNull(result);
  Assert.IsInstanceOfType(result, typeof(AppWindowManager));
}

Leider funktioniert das nicht weil der Bootstrapper wesentlich mehr im Hintergrund tut als es zunächst den Anschein hat und man durch einen direkten Zugriff auf die Methoden nicht alle Vorbedingungen erfüllt die auch für den Konstruktor notwendig sind. Genauer schlägt der Test bereits bei der Instanziierung des SUT durch eine NullReferenceException fehl, die aller Wahrscheinlichkeit daher rührt, dass er auf eine nicht vorhandene Application Instanz zugreift.

Wir haben also auf der einen Seite Funktionalität die eigentlich in sich abgeschlossen ist (Configure und CreateInstance) und getestet werden sollte. Auf der anderen Seite hingegen haben wir aber wiederum Abhängigkeiten die aufgelöst werden müssen obwohl sie für den eigentlichen Test nicht von Belang sind. Viel schlimmer noch: Wir kennen jene Abhängigkeiten nicht, da sie von außen nicht sichtbar sind.

Betrachtet man die Situation nun näher, fällt auf, dass wir eine Vermischung von Zuständigkeiten innerhalb des Bootstrappers haben. Natürlich macht es im gegebenen Fall Sinn, Methoden zu definieren die dann in der Start Methode nacheinander aufgerufen werden können. Es ist aber wenig sinnvoll jene auch einzig und allein im Bootstrapper auszuprogrammieren. Oder anders: Aufgabe des Bootstrappers ist die strukturierte Initialisierung unserer Applikation,  nicht aber die spezifische Initialisierung oder Realisierung eines Service Locators.

Dadurch, dass unsere Tests nicht einfach zu realisieren sind haben sie uns indirekt darauf hingewiesen, dass bei der Realisierung des Bootstrapper mehrere Verantwortlichkeiten vermischt werden. Statt uns also Gedanken darüber zu machen wie wir den Bootstrapper getestet kriegen, nur um es dann entnervt aufzugeben oder riesige Tests pflegen zu müssen, sollten wir die privaten Methoden als public in einer eigenen Klasse auslagern die dann vom Bootstrapper genutzt wird und ggf. sogar selbst einfacher austauschbar ist.

Bootstrapper mit InstanceConfiguration

Nun können wir die Tests viel einfacher umsetzen und trotzdem sicher sein, dass der Bootstrapper wie gewünscht funktioniert, da er die Anfragen nur an die Methoden der neuen Adapter-Klasse weiter leitet.

[TestMethod]
public void InstanceConfiguration_shall_return_instance_of_WindowManager()
{
  var sut = new InstanceConfiguration();
  sut.Configure();

  var instance = sut.GetInstance(typeof(IWindowManager), null);

  Assert.IsNotNull(instance);
  Assert.IsInstanceOfTypeinstance typeof(AppWindowManager));
}

Um die Sache also zusammen zu fassen, zitiere ich einmal frei nach Uncle Bob:

Wenn private Methoden so wichtig werden, dass sie getestet werden müssen, sollten sie in eine eigene Klasse ausgelagert werden.

Andernfalls ist es nämlich möglich sie über ihre öffentlichen Schnittstellen zu prüfen.


Kick It auf dotnet-kicks.de