How-To: Private und internal testen mit MS Test

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.

5 Kommentare

  1. Hallo Hendrik,

    sehr schöner Artikel jedoch habe ich einige Sachen die ich gern hier ansprechen würde. Ich würde Dir von Singletons abraten. Verwende hierfür lieber das Monostate Pattern. Warum erkläre ich im letzten Absatz des Artikels: http://danielminigshofer.blogspot.de/2012/03/static-methods-rip-tdd.html

    Private Felder zu testen halte ich für grob fahrlässig. Reflection ist und bleibt gefährlich. Was ist wenn ich ein automatisches Refactoring am Produktivcode durchführe (Rename)? Die Tests müssen dann alle per Hand angepasst werden. Arbeite hier besser mit DI oder SI. Somit bleibt der Test- und der Produktivcode sauber und verständlich.

    Tests wie Du sie oben beschrieben hast mit externen Resourcen sind außerdem keine Unit-Tests mehr. Solche Tests sind Integrationstest und sollten mit Mocks betrieben werden (http://danielminigshofer.blogspot.de/2012/03/10-anti-patterns-beim-testorientierten_20.html).

    Die Sache mit internal ist natürlich richtig und auch sehr nützlich. Hier gebe ich dir vollkommen Recht.

    Viele Grüße,
    Daniel

    1. Ich habe im Artikel nicht zwangsläufig von Unit Tests gesprochen, sondern von automatisierten Tests generell. Diese können auch in höheren Testebenen bis hoch zum Systemtest betrieben werden und dort sollte man nicht mocken da das Ziel in diesen Teststufen ein anderes ist.

      Darüber hinaus kann man in Bestandsprojekten, die ohne DI entwickelt wurden jenes auch nicht einfach nachrüsten. Will man darin also Bugs fixen hilft es manchmal einen Mock an der richtigen Stelle zu platzieren in dem man für den Test einfach ein privates Feld überschreibt.

      Nur am Rande: Wie du siehst wurde dein Kommentar nicht gelöscht. Ich hatte ihn nur nicht frei gegeben da ich erst heute wieder in die Adminkonsole des Blogs geschaut habe.

  2. Hallo Hendrik,

    dein Beispiel oben zeigt jedoch einen Integrationstest und keinen Systemtest. Deswegen bezieht sich mein Kommentar auf dein Beispiel.

    Laut „TDD by example“ sollte man lediglich die Public Schnittstellen testen (Zitat von Aman King (ThoughtWorks) Privacy is important. Encapsulation. Do not compromise it for tests. All tests should be written using only public protocol of the class under test. Wishing for white box testing is not a testing problem, it is a design problem.)

    Des Weiteren muss man Bestandsprojekte, die ohne DI entwickelt wurden erstmal testbar machen. Hier wäre es aber grob fahrlässig dann mittels Reflection Tests zu schreiben. Da man hier sehr viel mit automatischen Refactorings arbeitet.

    BTW bei Systemtests würde ich diese in einen andere Kategorie oder in der Testliste in eine andere Sparte verschieben. Hier macht es nur bedingt Sinn Mocks zu verwenden.

    Viele Grüße,
    Daniel

Kommentar hinterlassen