DataBinding ist beliebt. Lästig daran ist: Man muss die INotifyPropertyChanged-Schnittstelle implementieren. Sie fordert, dass bei Änderungen an den Eigenschaften eines Objekts das Ereignis PropertyChanged ausgelöst wird. Dabei muss dem Ereignis der Name der geänderten Eigenschaft als Parameter in Form einer Zeichenkette übergeben werden. Die Frage, die uns diesmal beim dotnetpro.dojo interessiert, ist: Wie kann man die Implementierung der INotifyPropertyChanged-Schnittstelle automatisiert testen?
Die Funktionsweise des Events für eine einzelne Eigenschaft zu prüfen ist nicht schwer. Man bindet einen Delegate an den Property-Changed-Event und prüft, ob erbeiÄnderung der Eigenschaft aufgerufen wird. Außerdem ist zu prüfen, ob der übergebene Name der Eigenschaft korrekt ist, siehe Listing 3.
Listing 3: Property changed?
[Test]
public void Name_Property_loest_PropertyChanged_Event_korrekt_aus() {
var kunde = new Kunde();
var count = 0;
kunde.PropertyChanged += (o, e) => {
count++;
Assert.That(e.PropertyName, Is.EqualTo("Name"));
};
kunde.Name = "Stefan"; Assert.That(count,Is.EqualTo(1));
}
Um zu prüfen, ob der Delegate aufgerufen wurde, erhöhen Sie im Delegate beispielsweise eine Variable, die außerhalb definiert ist. Durch diesen Seiteneffekt können Sie überprüfen, ob der Event beim Ändern der Eigenschaft ausgelöst und dadurch der Delegate aufgerufen wurde. Den Namen der Eigenschaft prüfen Sie innerhalb des Delegates mit einem Assert.
Solche Tests für jede Eigenschaft und jede Klasse, die INotifyPropertyChanged implementiert, zu schreiben, wäre keine Lösung, weil Sie dabei Code wiederholen würden. Da die Eigenschaften einer Klasse per Reflection ermittelt werden können, ist es nicht schwer, den Testcode so zu verallgemeinern, dass damit alle Eigenschaften einer Klasse getestet werden können. Also lautet in diesem Monat die Aufgabe: Implementieren Sie eine Klasse zum automatisierten Testen der INotifyPropertyChanged-Logik. Die zu implementierende Funktionalität ist einWerkzeug zum Testen von ViewModels. Dieses Werkzeug soll wie folgt bedient werden:
NotificationTester.Verify<MyViewModel>();
Die Klasse, die auf INotifyPropertyChanged-Semantik geprüft werden soll, wird als generischer Typparameter an die Methode übergeben. Die Prüfung soll so erfolgen, dass per Reflection alle Eigenschaften der Klasse gesuchtwerden, die über einen Setter und Getter verfügen. Für diese Eigenschaften soll geprüft werden, ob sie bei einer Zuweisung an die Eigenschaft den PropertyChanged-Event auslösen und dabei den Namen der Eigenschaft korrekt übergeben. Wird der Event nicht korrekt ausgelöst, muss eine Ausnahme ausgelöst werden. Diese führt bei der Ausführung des Tests durch das Unit-Test-Frame-work zum Scheitern des Tests.
Damit man weiß, für welche Eigenschaft die Logik nicht korrekt implementiert ist, sollte die Ausnahme mit den notwendigen Informationen ausgestattet werden, also dem Namen der Klasse und der Eigenschaft, für die der Test fehlschlug.
In einer weiteren Ausbaustufe könnte das Werkzeug dann auch auf Klassen angewandt werden, die ebenfalls per Reflection ermittelt wurden. Fasst man beispielsweise sämtliche ViewModels in einem bestimmten Namespace zusammen, kann eine Assembly nach ViewModels durchsucht werden. Damit die so gefundenen Klassen überprüft werden können, muss es möglich sein, das Testwerkzeug auch mit einem Typ als Parameter aufzurufen :
NotificationTester.Verify (typeof(MyViewModel));
Im nächsten Heft finden Sie eine Lösung des Problems. Aber versuchen Sie sich zunächst selbst an der Aufgabe. [ml]
LÖSUNG INotifyPropertyChanged-Logik automatisiert testen |
Kettenreaktion
Das automatisierte Testen der INotifyPropertyChanged-Logik ist nicht schwer. Man nehme einen Test, verallgemeinere ihn, streue eine Prise Reflection darüber, fertig. Doch wie zerlegt man die Aufgabenstellung so in Funktionseinheiten, dass diese jeweils genau eine definierte Verantwortlichkeit haben? Die Antwort: Suche den Flow!
Wie man die INotifyPropertyChanged-Logik automatisiert testen kann, habe ich in der Aufgabenstellung zu dieser Übung bereits gezeigt [1]. Doch wie verallgemeinert man nun diesen Test so, dass er für alle Eigenschaften einer Klasse automatisiert ausgeführt wird?
Im Kern basiert die Lösung auf folgender Idee: Suche per Reflection alle Properties einer Klasse und führe den Test für die gefundenen Properties aus. Klingt einfach, ist es auch. Aber halt: Bitte greifen Sie nicht sofort zur Konsole! Auch bei vermeintlich unkomplizierten Aufgabenstellungen lohnt es sich, das Problem so zu zerlegen, dass kleine, überschaubare Funktionseinheiten mit einer klar abgegrenzten Verantwortlichkeit entstehen.
Suche den Flow!
Ich möchte versuchen, die Aufgabenstellung mit einem Flow zu lösen. Doch dazu sollte ich ein klein wenig ausholen und zunächst erläutern, was ein Flow ist und wo seine Vorteile liegen.
Vereinfacht gesagt ist ein Flow eine Aneinanderreihung von Funktionen. Ein Argument geht in die erste Funktion hinein, diese berechnet damit etwas und liefert ein Ergebnis zurück. Dieses Ergebnis geht in die nächste Funktion, auch diese berechnet damit wieder etwas und liefert ihr Ergebnis an die nächste Funktion. Auf diesem Weg wird ein Eingangswert nach und nach zu einem Ergebnis transformiert, siehe Listing 1.
Listing 1: Ein einfacher Flow.
var input = "input";
var x1 = A(input);
var x2 = B(x1);
var result = C(x2);
Die einzelnen Funktionen innerhalb eines Flows, die sogenannten Flowstages, sind zustandslos, das heißt, sie erledigen ihre Aufgabe ausschließlich mit den Daten aus ihren Argumenten. Das hat den Vorteil, dass mehrere Flows asynchron ausgeführt werden können, ohne dass dabei die Zugriffe auf den Zustand synchronisiert werden müssten. Ferner lassen sich zustandslose Funktionen sehr schön automatisiert testen, weil das Ergebnis eben nur von den Eingangsparametern abhängt.
Einer nach dem anderen
Ein Detail ist bei der Realisierung von Flows ganz wichtig: Weitergereicht werden sollten nach Möglichkeit jeweils Daten vom Typ IEnumerable<T>. Dadurch besteht nämlich die Möglichkeit, auf diesen Daten mit LINQ zu operieren. Ferner können die einzelnen Flowstages dann beliebig große Datenmengen verarbeiten, da bei Verwendung von IEnumerable<T> nicht alle Daten vollständig im Speicher existieren müssen, sondern Element für Element bereitgestellt werden können. Im Idealfall fließt also zwischen den einzelnen Flowstages immer nur ein einzelnes Element. Es wird nicht etwa das gesamte Ergebnis der ersten Stage berechnet und dann vollständig weitergeleitet.
Im Beispiel von Listing 2 führt die Verwendung von yield return dazu, dass der Compiler einen Enumerator erzeugt. Dieser Enumerator liefert nicht sofort die gesamte Aufzählung, sondern stellt auf Anfrage Wert für Wert bereit. Bei Ausführung der Methode Flow() werden also zunächst nur die einzelnen Aufzählungen und Funktionen miteinander verbunden. Erst wenn das erste Element aus dem Ergebnis entnommen werden soll, beginnen die Enumerato-ren, Werte zu liefern. Der Flow kommt also erst dann in Gang, wenn jemand hinten das erste Element „herauszieht“.
Listing 2: Rückgabedaten