Скачать книгу

public void Flow() {

       var input = Input();

       var x1 = A(input);

       var x2 = B(x1);

       var result = C(x2);

       foreach(var value in result) {

       ...

       }

       }

       public IEnumerable<string> Input() {

       yield return "Äpfel";

       yield return "Birnen";

       yield return "Pflaumen";

       }

       public IEnumerable<string> A(IEnumerable<string> input) {

       foreach (var value in input) {

       yield return string.Format("({0})", value);

       }

       }

       public IEnumerable<string> B(IEnumerable<string> input) {

       foreach (var value in input) {

       yield return string.Format("[{0}]", value);

       }

       }

       public IEnumerable<string> C(IEnumerable<string> input) {

       foreach (var value in input) {

       yield return string.Format("-{0}-", value);

       }

       }

      Als erste ist die Funktion C an der Reihe. Sie entnimmt aus der ihr übergebenen Aufzählung x2 das erste Element. Dadurch kommt B ins Spiel und entnimmt ihrerseits der Aufzählung x1 den ersten Wert. Dies setzt sich fort, bis die Methode Input den ersten Wert liefern muss. Im Flow werden die einzelnen Werte sozusagen von hinten durch den Flow gezogen. Ein Flow bietet in Verbindung mit IEnumerable<T> und yield return die Möglichkeit, unendlich große Datenmengen zu verarbeiten, ohne dass eine einzelne Flowstage die Daten komplett im Speicher halten muss.

      Lesbarkeit durch Extension Methods

      Verwendet man bei der Implementierung der Flowstages Extension Methods, kann man die einzelnen Stages syntaktisch hintereinanderschreiben, sodass der Flow im Code deutlich in Erscheinung tritt. Dazu muss lediglich der erste Parameter der Funktion um das Schlüsselwort this ergänzt werden, siehe Listing 3. Natürlich müssen die Parameter und Return-Typen der Flowstages zueinander passen.

      Listing 3: Die Stages syntaktisch koppeln.

       public static IEnumerable<string> A(this IEnumerable<string> input) {

       foreach (var value in input) {

       yield return string.Format("({0})", value);

       }

       }

       ...

       var result = Input().A().B().C();

      Lösungsansatz

      Der erste Schritt des INotifyProperty-Changed-Testers besteht darin, die zu testenden Properties des Typs zu ermitteln. Anschließend muss er jedem dieser Properties einen Wert zuweisen, um zu prüfen, ob der Event korrekt ausgelöst wird. Zum Zuweisen eines Wertes benötigen Sie zur Laufzeit einen Wert vom Typ der Property. Wenn Sie auf eine string-Property stoßen, müssen Sie einen string-Wert instanzieren, das ist einfach.

      Komplizierter wird die Sache, wenn der Typ der Property ein komplexer Typ ist. Denken Sie etwa an eine Liste von Points oder Ähnliches. Richtig knifflig wird es, wenn der Typ der Property ein Interfacetyp ist. Dann ist eine unmittelbare Instanzie-rung nicht möglich. Das Instanzieren der Werte scheint eine eigenständige Funktionseinheit zu sein, denn die Aufgabe ist recht umfangreich.

      Wenn Sie die Properties und ihren jeweiligen Typ gefunden haben, müssen Sie für jede Property einen Test ausführen. Jeder dieser Tests ist eine Action<object>, die auf einer Instanz der Klasse ausgeführt wird, die zu testen ist. Wenn also die Klasse KundeViewModel überprüft werden soll, wird für jede Property eine Action<KundeView-Model> erzeugt. Sind die Actions erzeugt, müssen sie nur nacheinander ausgeführt werden. Dabei soll jede Action eine neue Instanz der zu testenden Klasse erhalten. Andernfalls könnte es zu Seiteneffekten beim Testen der Properties kommen.

      Funktionseinheiten identifizieren

      Die erste Aufgabe ist also das Ermitteln der zu testenden Properties. Eingangsparameter in diese Funktionseinheit ist der Typ, für den die INotifyPropertyChanged-Implementierung überprüft werden soll. Das Ergebnis der Flowstage ist eine Aufzählung der Property-Namen.

      static IEnumerable<string>

       FindPropertyNames(this Type type)

      An dieser Stelle fragen Sie sich möglicherweise, warum ich die Property-Namen als Strings zurückgebe und nicht etwa eine Liste von PropertyInfo-Objekten. Schließlich stecken in PropertyInfo mehr Informationen, insbesondere der Typ der Property, den ich später ebenfalls benötige. Ich habe mich dagegen entschieden, weil dies das Testen der nächsten Flowstage deutlich erschwert hätte. Denn diese hätte dann auf einer Liste von PropertyInfo-Objekten arbeiten müssen. Und da PropertyInfo-Instanzen nicht einfach mit new hergestellt werden können, wären die Tests recht mühsam geworden.

      Nachdem die Property-Namen bekannt sind, kann die nächste Flowstage dazu den jeweiligen Typ ermitteln. Die Flowstage erhält also eine Liste von Property-Namen sowie den Typ und liefert eine Aufzählung von Typen.

      static IEnumerable<Type> FindPropertyTypes(

       this IEnumerable<string> propertyNames,

       Type type)

      Im Anschluss muss für jeden Typ ein Objekt instanziert werden. Diese Objekte werden später im Test den Properties zugewiesen. Die Flowstage erhält also eine Liste von Typen und liefert für jeden dieser Typen eine Instanz des entsprechenden Typs.

      static IEnumerable<object> GenerateValues(

       this IEnumerable<Type> types)

      Dann wird es spannend: Die Actions müssen erzeugt werden. Dabei lässt es sich leider nicht vermeiden, die Property-Namen aus der ersten Stage nochmals zu verwenden. Die Ergebnisse der ersten Stage fließen also nicht nur in die unmittelbar nächste Stage, sondern zusätzlich auch noch in die Stage, welche die Actions erzeugt. Die Namen der Properties werden benötigt, um mittels Reflection die jeweiligen Setter aufrufen zu können.

      static IEnumerable<Action<object>>

       GenerateTestMethods(this IEnumerable<object>

       values, IEnumerable<string> propertyNames,

       Type type)

      Der letzte Schritt besteht darin, die gelieferten Actions auszuführen. Dazu muss jeweils eine Instanz der zu testenden Klasse erzeugt und an die Action übergeben werden.

      Abbildung 2 zeigt den gesamten Flow. Die einzelnen Flowstages sind als Extension Method implementiert. Der Flow selbst wird in der öffentlichen Methode Notifica-tionTester.Verify zusammengesteckt. Testen möchte ich die einzelnen Stages aber isoliert. Denn nur so kann ich die Implementierung Schritt für Schritt vorantreiben und muss nicht gleich einen Integrationstest für den gesamten Flow schreiben. Einige Integrationstests sollten am Ende aber auch nicht fehlen.

      Diese Vorgehensweise hat einen weiteren Vorteil: Um den NotificationTester testen zu können, müssen Testdaten her. Da er auf Typen arbeitet, müssen also Testdaten in Form von Klassen erstellt werden. Das ist nicht nur aufwendig, sondern wird auch schnell unübersichtlich. Ganz kommt man zwar am Erstellen solcher Testklassen auch nicht vorbei, aber der Aufwand ist doch reduziert.

Скачать книгу