Dojos für Entwickler 2. Stefan Lieser
Entwurf für die erste Iteration zeigt Abbildung 2. Ich gehe in drei Schritten vor:
Die Dateinamen werden ermittelt.
Zu den gefundenen Dateinamen werden die Stichwörter ermittelt.
Die gefundenen Stichwörter werden so gefiltert, dass nur eindeutige Stichwörter übrig bleiben.
[Abb. 2]
Stichwörter mit dem Windows Explorer vergeben.
Mal nicht Test-first
Durch die drei im Entwurf gezeigten Bauteile habe ich mich von oben nach unten durchgearbeitet. Dabei habe ich diesmal nicht Tests zuerst geschrieben, sondern zuerst implementiert und dann Tests geschrieben. Das hat hier gut funktioniert, da durch den Entwurf klar war, was die Bauteile machen sollen. Ferner gibt es keine Abhängigkeiten, sodass die Tests flüssig von der Hand gingen. Zu beachten ist allerdings, dass zwei von den drei Bauteilen von der externen Ressource Dateisystem beziehungsweise von JPEG-Dateien abhängig sind. Dies musste ich natürlich bei den automatisierten Tests berücksichtigen.
Listing 3 zeigt die Implementation für das Bauteil Dateinamen_suchen.
Die eigentliche Arbeit übernimmt die statische Methode Directory.EnumerateFiles aus dem .NET Framework. Drumherum ist lediglich eine Event Based Component (EBC) gelegt. Die Komponente besteht aus der Input-Methode Process und dem Output-Event Result. Da das Bauteil für seine Arbeit zwei Angaben benötigt, den Pfad und das Suchmuster, werden diese zu einem Tuple<string, string> zusammengefasst. Dadurch hat die Process-Methode lediglich einen Parameter. Das ist wichtig, um Standardbausteine und Tooling verwenden zu können. In diesem Fall ist das zwar nicht relevant, aber es könnte sein, dass eine spätere Iteration den Einsatz von Standardbausteinen wie etwa Join sinnvoll erscheinen lässt. Daher ist es eine Tugend, mit nur einem Parameter zu arbeiten.
Um innerhalb der Methode zu erkennen, welche Bedeutung Item1 und Item2 des Tupels haben, weise ich beide jeweils an eine lokale Variable zu. So kann ein sprechender Bezeichner vergeben werden, aus dem sich die Bedeutung erschließt.
Listing 3
Dateinamen suchen.
public class Dateinamen_suchen { public void Process(Tuple<string, string> path_und_SearchPattern) { var path = path_und_SearchPattern.Item1; var searchPattern = path_und_SearchPattern.Item2; var filenames = Directory.EnumerateFiles(path, searchPattern, SearchOption.AllDirectories); Result(filenames); } public event Action< IEnumerable<string>> Result; }
Es ergibt sich nun die Frage, wie man das Bauteil automatisiert testen kann. Seine Aufgabe ist es, zu einem gegebenen Pfad und einem Suchmuster die Liste der gefundenen Dateinamen zu liefern. Folglich werden für einen automatisierten Test Testdaten benötigt. Diese habe ich im Testprojekt angelegt, wie Abbildung 3 zeigt.
[Abb. 3]
Testdaten zum automatisierten Testen der Dateisuche.
Ganz wichtig: Bei den Testdateien test1.txt, test2.txt et cetera muss die Eigenschaft Copy to Output Directory auf Copy always oder Copy if newer eingestellt werden. Dadurch werden die Testdateien samt Verzeichnisstruktur in das Ausgabeverzeichnis des Projektes kopiert. Im Test können die Dateien dann über den relativen Pfad testdaten angesprochen werden, wie Listing 4 zeigt. Wird im Verzeichnis testdaten nach den Dateien *.txt gesucht, müssen drei Dateinamen geliefert werden. Glücklicherweise liefert Directory.EnumerateFiles die Dateinamen als relative Namen. Wären es absolute Pfade, wäre eine Automatisierung schwieriger, da dann der absolute Pfad der jeweiligen Testumgebung zu berücksichtigen wäre.
Listing 4
Test: Dateinamen
[TestFixture] public class Dateinamen_suchen_Tests { private Dateinamen_suchen sut; private IEnumerable<string> result; [SetUp] public void Setup() { sut = new Dateinamen_suchen(); sut.Result += x => result = x; } [Test] public void Txt_Dateien_rekursiv_suchen() { sut.Process( new Tuple<string, string>( @"testdaten", "*.txt")); Assert.That(result.ToArray(), Is.EqualTo(new[] { @"testdaten\test1.txt", @"testdaten\test2.txt", @"testdaten\ebene2\test4.txt" })); } }
Der Spike zahlt sich aus
Als Nächstes kam das Bauteil Alle_Stichwörter_ermitteln an die Reihe. Auch hier habe ich erst implementiert und dann getestet. Zur Enttäuschung der Test-first-Verfechter muss ich auch hier mitteilen, dass das nachträgliche Schreiben der automatisierten Tests keine Probleme bereitet hat. Das verwundert mich natürlich nicht wirklich, denn vor der Implementation stand ja der Entwurf mit Flow-Design. Ich möchte hier nicht den Eindruck erwecken, dass Test-first eine überflüssige Praktik sei. Allerdings bringt es eben auch keinen Vorteil, sich sklavisch an diese Praktik zu halten. In vielen Fällen ist eine Test-first-Vorgehensweise sinnvoll, nämlich immer dann, wenn Unsicherheiten bei der Implementation vorhanden sind. Durch den Entwurf und den Spike war mir allerdings zu Beginn der Implementation klar, was auf mich zukommt. Während der Übungszeit Praktiken bewusst einmal anzuwenden und ein anderes Mal nicht, halte ich für eine nützliche Form der Reflexion.
Beim Ermitteln der Stichwörter habe ich auf die Erkenntnisse aus dem Spike zurückgegriffen. Die Aufgabe bestand darin, zu einer Aufzählung von Dateinamen alle verwendeten Stichwörter zu liefern. Durch die Verwendung von yield return sieht die Implementation recht übersichtlich aus, wie Listing 5 zeigt.
Listing 5
Stichwörter ermitteln.
public class Alle_Stichwörter_ermitteln { public void Process(IEnumerable< string> dateinamen) { Result(Stichwörter_ermitteln(dateinamen)); } private IEnumerable<string> Stichwörter_ermitteln( IEnumerable<string> dateinamen) { foreach (var dateiname in dateinamen) { var stichwörter = Stichwörter _ermitteln(dateiname); foreach (var stichwort in stichwörter) { yield return stichwort; } } } private IEnumerable<string< Stichwörter_ermitteln(string dateiname) { BitmapSource img = BitmapFrame.Create( new Uri(dateiname, UriKind.Relative)); var metadata = (BitmapMetadata)img.Metadata; if (metadata.Keywords == null) { yield break; } foreach (var keyword in metadata.Keywords) { yield return keyword; } } public event Action<IEnumerable< string>> Result; }
Beim Aufruf der Process-Methode wird die gesamte Aufzählung der Dateinamen an die Methode Stichwörter_ermitteln übergeben. Diese hat einen Return-Wert vom Typ IEnumerable<string>, sodass yield return verwendet werden kann. Das war mein Ziel. Doch die Methode erledigt nicht die gesamte Arbeit, sondern lässt eine Überladung von Stichwörter_ermitteln die Arbeit für eine einzelne Datei ausführen. Dafür habe ich mich entschieden, damit die Methoden übersichtlich und klein bleiben. Die erste Überladung iteriert über alle Dateinamen und über die gelieferten Stichwörter in Form von zwei geschachtelten foreach-Schleifen. Listing 6 zeigt, dass das mithilfe von LINQ deutlich knackiger geht. Ich höre allerdings schon die Kritiker rufen: „Das versteht doch keiner mehr.“ Mag sein. Wer sich bislang nicht intensiv mit LINQ auseinandergesetzt hat, wird mit dieser knappen Formulierung möglicherweise Probleme haben. Aber wir programmieren ja auch nicht mehr in Assembler, nur weil irgendjemand C-Code nicht lesen kann.
Listing 6
LINQ nutzen.
private IEnumerable<string> Stichwörter_ermitteln( IEnumerable<string> dateinamen) { return dateinamen.SelectMany( Stichwörter_ermitteln); }
Um nun dieses Bauteil automatisiert testen zu können, bedarf es wieder Testdaten in Form von JPEG-Dateien. Auch diese Dateien habe ich im Projekt