Dojos für Entwickler. Stefan Lieser
ermitteln.
Interna testbar machen
Um die einzelnen Flowstages isoliert testen zu können, habe ich ihre Sichtbarkeit auf internal gesetzt. Damit sind die Methoden zunächst nur innerhalb der Assembly, in der sie implementiert sind, sichtbar. Um auch in der Test-Assembly darauf zugreifen zu können, muss diese zusätzliche Sichtbarkeit über das Attribut InternalsVisibleTo hergestellt werden:
[assembly:InternalsVisibleTo(
"INotifyTester.Tests")]
Das Attribut kann prinzipiell in einer beliebigen Quellcodedatei in der Assembly untergebracht werden. Üblicherweise werden Attribute, die sich auf die Assembly beziehen, in der Datei AssemblyInfo.cs untergebracht. Diese finden Sie im Visual Studio Solution Explorer innerhalb des Ordners Properties.
Das Sichtbarmachen der internen Methoden nur zum Zwecke des Testens halte ich auf diese Weise für vertretbar. UnitTests sind Whitebox-Tests, das heißt, die Art und Weise der Implementierung ist bekannt. Im Gegensatz dazu stehen Blackbox-Tests, die ganz bewusst keine Annahmen über den inneren Aufbau der zu testenden Funktionseinheiten machen. Durch Verwendung von internal ist die Sichtbarkeit nur so weit erhöht, dass die Methoden in Tests angesprochen werden können. Eine vollständige Offenlegung mit public wäre mir zu viel des Guten. Übrigens halte ich es für keine gute Idee, auf die Interna einer zu testenden Klasse mittels Reflection zuzugreifen. Dabei entziehen sich nämlich die Interna, die über Reflection angesprochen werden, den Refaktori-sierungswerkzeugen. Und wie man sieht, ist internal in Verbindung mit dem InternalsVisibleTo-Attribut völlig ausreichend.
FindPropertyNames
Die Namen der Properties werden durch die Flowstage FindPropertyNames geliefert. Dabei entscheidet diese Funktion bereits, welche Properties geprüft werden sollen. Es werden nur Properties berücksichtigt, die über öffentliche Getter und Setter verfügen.
Um diese Funktion testen zu können, müssen Testklassen angelegt werden. Das lässt sich leider nicht vermeiden, da die Funktion auf einem Typ als Argument arbeitet. Bei der testgetriebenen Entwicklung steht der Test vor der Implementierung, also gilt es, Testdaten zu erstellen. Ich habe mich zunächst um das „Happy Day Szenario“ gekümmert, also einen Testfall, der später bei der Verwendung typisch ist, siehe Listing 4.
Listing 4: Eine einfache Testklasse.
Ledigpublic
class ClassWithPublicGettersAndSetters
{
public string StringProperty { get; set;}
public int IntProperty { get; set;}
}
Als Nächstes folgt eine Klasse, deren Properties private sind. Diese sollen in den Tests unberücksichtigt bleiben, ihr Name darf also nicht geliefert werden. Die Implementierung der Funktion ist mit LINQ ganz einfach, siehe Listing 5.
Listing 5: Die zu prüfenden Properties finden.
internal static IEnumerable<string> FindPropertyNames(Type type) {
return type.GetProperties(PropertyBindingFlags)
.Where(propertyInfo => propertyInfo.CanRead)
.Where(propertyInfo => propertyInfo.CanWrite)
.Select(propertyInfo => propertyInfo.Name);
}
Die beiden Where-Klauseln sorgen dafür, dass nur Properties berücksichtigt werden, die sowohl einen Getter als auch einen Setter haben. Durch die Binding Flags werden schon Properties ausgeschlossen, die nicht public sind. Durch die Select-Klausel wird festgelegt, wie die zu liefernden Ergebnisse aufgebaut sein sollen.
FindPropertyTypes
Die Funktion FindPropertyTypes erhält als Argumente die Liste der Property-Namen, die berücksichtigt werden sollen, sowie den Typ, zu dem die Properties gehören. Dazu liefert sie jeweils den Typ der Properties. Auch diese Tests benötigen wieder Testklassen. Ich habe einfach die schon vorhandenen Testklassen verwendet. Auch hier ist die Implementierung dank LINQ nicht schwierig.
GenerateValues
Um die Property-Setter später aufrufen zu können, muss jeweils ein Objekt vom Typ der Property erzeugt werden. Diese Aufgabe übernimmt die Funktion GenerateValues. Sie erhält als Argument die Liste der Typen und liefert dazu jeweils eine Instanz. Die Funktion ist derzeit recht einfach gehalten. Die Instanz wird einfach durch Verwendung von Activator.CreateInstance erzeugt. Lediglich Strings werden gesondert behandelt, da die Klasse über keinen parameterlosen Konstruktor verfügt, siehe Listing 6.
Listing 6: Passende Objekte erzeugen.
internal static IEnumerable<object> GenerateValues(this IEnumerable<Type> types) {
return types.Select(type => CreateInstance(type));
}
internal static object CreateInstance(Type type) {
if (type == typeof(string)) {
return "";
}
return Activator.CreateInstance(type);
}
Die Methode CreateInstance muss sicher im Laufe der Zeit angepasst werden. Sie ist in der gezeigten Implementierung nicht in der Lage, mit komplexen Typen zurechtzukommen.
GenerateTestMethods
Nun stehen alle Informationen zur Verfügung, um für jede Property eine Testmethode zu erzeugen. Die Funktion Generate-TestMethods erhält drei Argumente:
die Liste der Werte für die Zuweisung, I die Liste der Property-Namen,
den Typ, auf den sich die Tests beziehen.
Das Ergebnis ist eine Liste von Actions.
static IEnumerable<Action<object>>
GenerateTestMethods(this IEnumerable<object>
values, IEnumerable<string> propertyNames,
Type type)
Das Testen dieser Funktion kommt leider auch wieder nicht ohne Testklassen aus, denn der Typ geht ja als Argument in die Funktion ein. Die erzeugten Testmethoden werden im Test aufgerufen, um so zu prüfen, dass sie jeweils einen bestimmten Aspekt der INotifyPropertyChanged-Semantik überprüfen. Hier wird es schon schwierig, die Vorgehensweise zu beschreiben, da es sich um Tests handelt, die testen, dass generierte Testmethoden richtig testen, sozusagen Metatests.
Die Implementierung der Funktion hat es ebenfalls in sich. Zunächst müssen zwei Aufzählungen „im Gleichschritt“ durchlaufen werden. Dazu wird der Enumerator einer der beiden Aufzählungen ermittelt. Anschließend wird der andere Enumerator in einer foreach-Schleife durchlaufen. Innerhalb der Schleife wird der erste Enumerator dann „per Hand“ mit MoveNext und Current bedient. Ich hätte dies gerne in eine Methode ausgelagert, das ist jedoch durch die Verwendung von yield return nicht möglich.
Damit sind wir bei der zweiten Besonderheit der Funktion. Die einzelnen Testmethoden werden jeweils mit yield return zurückgeliefert. Da das Ergebnis der Funktion eine Aufzählung von Actions ist, liefert das yield return jeweils eine Action in Form einer Lambda Expression. Dabei müssen die Werte, die aus den Enumeratoren in der Schleife entnommen werden, in lokalen Variablen abgelegt werden, damit sie als Closure in die Lambda Expression eingehen können. Andernfalls würden am Ende alle Lambda Expressions auf demselben Wert arbeiten, nämlich dem aus dem letzten Schleifendurchlauf. Auch hier macht sich übrigens wieder mal der Einsatz von JetBrains ReSharper bezahlt. Der weist nämlich mit der Warnung „Access to modified closure“ auf das Problem hin.
ExecuteTestMethods