Einer der viel gesprochenen Leitsätze des Testens ist: „Finger weg von privaten Membern“. Der Gedanke hinter dieser Aussage ist einleuchtend, denn je mehr Aussagen ich in einem Test über die Implementierung mache, desto höher die Wahscheinlichkeit, dass ich ihn später anpassen muss oder er fehl schlägt.
Nichts desto trotz, kann man sich durch den Zugriff auf private Member gelegentlich viel Arbeit sparen wenn es darum geht einen Test aufzusetzen und außerdem hilft es manchmal sogar beim Aufspüren von Bugs. Weiterhin ist es teils unumgänglich auch Dinge zu testen die als internal gekennzeichnet sind und demnach theoretisch nicht vom Testprojekt identifiziert werden könnten.
Internal
Für letzteres Problem gibt es gleich auch die schnellste Lösung, welche selbst über das Testen hinaus nützlich sein kann. So verfügt jedes Projekt einer Solution über eine Datei AssemblyInfo.cs. Darin enthalten sind Informationen wie die Versionsnummer, die eindeutige Guid der Assembly usw., zusammengefasst in einer Attributschreibweise.
In diese Datei kann das Attribut InternalsVissibleTo eingefügt werden welches den kompletten Namen der „befreundeten“ Dll übergeben wird. Nach dem nächsten Kompilieren sind dann alle als internal gekennzeichneten Klassen, Methode etc. auch innerhalb des anderen Projekts abrufbar.
[assembly: InternalsVisibleTo("[Friendly.Name.Dll]");
Leider kann das Attribute immer nur für die gesamte Assembly deklariert werden. Eine selektive Freigabe für einzelne Klassen ist somit nicht möglich.
Private Member
Auf private Objekte zuzugreifen ist zumindest im Rahmen von Tests nicht viel schwieriger. So kann man natürlich selbst Reflection bemühen um Methoden aufzurufen o.ä. man kann dies aber auch einfach die Klasse PrivateObject erledigen lassen. Diese bekommt bei der Instanzierung den Typ oder gleich eine Referenz der zu kapselnden Klasse übergeben und bietet dann den vollen Zugriff auf alle Eigenschaften.
So verfügt sie über diverse Set- bzw. Get-Methoden mit denen Werte auf Eigenschaften und Felder gesetzten oder von ihnen gelesen werden. Darüber hinaus erlauben die zahlreichen Überladungen von Invoke das Ausführen einzelner Methoden oder Auslesen deren Rückgabewerte.
So wird im folgenden Beispiel ein Wrapper um den Testgegenstand (sut) gelegt, mit dem erst ein Pfad zu einer bestimmten Datei hinterlegt wird. Dann rufen wir eine interne Methode zum Laden der Dataen auf um sie danach auf Richtigkeit zu prüfen.
var accessor = new PrivateObject(sut); accessor.SetField("_path", BindingFlags.SetField, "testdata.xml"); accessor.Invoke("Load_Internal"); IEnumerable<Data> loadedData = accessor.GetProperty("InternalData"); Assert.IsNotNull(loadedData);
Eine Einschränkung in Sachen Privatsphäre ergibt sich für PrivateObject in Sachen Vererbung. So ist es nicht möglich auf private Member einer Elternklasse zuzugreifen, weshalb das folgende Beispiel fehlschlägt. Der Wrapper der sich durch die Klasse ergibt, hat immer nur die gleichen Zugriffsrechte wie die Klasse die er kapselt. Würde im Beispiel der String also protected gekennzeichnet werden, ergäbe sich auch das erwünschte Ergebnis.
public class A { private string a = "Hallo"; } public class B : A { } [TestClass] public class Test { [TestMethod] public void TestCase() { var sut = new B(); var privateObject = new PrivateObject(sut); var result = privateObject.GetField("a").ToString(); Assert.AreEqual("Hallo", result); } }
Private statische Member
Einer der Hauptgründe „klassische“ Singletons nicht zu verwenden, ist das Hindernis welches sie in Sachen Test darstellen. So führen sie zu Abhängigkeiten die nur schwer erkennbar und noch schwerer aufzubrechen sind.
Eine Klasse die hierbei hilft ist PrivateType. Dabei handelt es sich quasi um einen Bruder von PrivateObject, der den Zugriff auf alle privaten statischen Elemente einer Klasse zulässt und dabei dem gleichen Vorgehensmuster folgt wie wie PrivateObject. Zu sehen auch im folgenden Beispiel sieht:
var accessor = new PrivateType(typeof(StaticSut)); accessor.SetStaticField("_path", BindingFlags.SetField, "testdata.xml"); accessor.InvokeStatic("Load_Internal"); IEnumerable<Data> loadedData = accessor.GetStaticProperty("InternalData"); Assert.IsNotNull(loadedData);
Accessor-Klassen des Visual Studio
Ein Nachteil von PrivateObject und PrivateType wird recht schnell bei der ersten Nutzung ersichtlich. Zwar kann man über die BindingFlags der Methoden einige Probleme ausschließen, dennoch neigt man bei den Angabe in Form von Strings sehr schnell zu Fehlern und das vor allem dann, wenn der Code refaktorisiert wird. Denn Magic-Strings sind bekanntlich nicht typsicher wodurch sie nur schlecht von diversen Tools analysiert werden können.
Aus diesem Grund bietet Visual Studio die Möglichkeit so genannte Accessor-Klassen zu generieren. Sie stellen automatisch alle privaten Member öffentlich zur Verfügung, aktualisieren sich selbst bei Änderungen am Code der eigentlichen Klasse und sind somit gänzlich typsicher, ABER laut Microsoft auch seit spätestens Visual Studio 2011 als deprecated zu betrachten.
Der Grund hierfür ist wohl, dass der dahinter liegende Generator mit dem steten Wandel in .Net nicht Schritt halten konnte/kann und seine Weiterentwicklung zumindest eingefrohren wurde. Das Feature existiert demnach hauptsächlich noch aus Kompatibilitätsgründen und könnte bei Portierungen auf höhere Framework- und VS-Versionen unter Umständen unangenehme Nebeneffekte aufweisen, weshalb sie besser nicht mehr verwendet werden sollten.
Sollen sie dennoch verwendet werden gibt es zwei Möglichkeiten einen Accessor zu erzeugen. So reicht es entweder die entsprechende Klasse im Editor zu öffnen und dort im Kontext-Menü „Create Private Accessor“ auszuwählen oder sich einen Unit Test über den entsprechenden Wizard zu erzeugen. Letzteres generiert jedoch eine ganze Menge Code den man meist nicht braucht, weshalb ich dies eher ungern nutze.
Kommentare