Dojos für Entwickler. Stefan Lieser
RandomIntGenerator : IGenerator<object>
{
private readonly int minimum;
private readonly int maximum;
private readonly Random random;
...
public object GenerateValue() {
return random.Next(minimum, maximum + 1);
}
}
Listing 7: Den Zufallsgenerator testen.
[TestFixture]
public class RandomIntGeneratorTests
{
private RandomIntGenerator sut;
[SetUp]
public void Setup() {
sut = new RandomIntGenerator(1, 5, new Random(0));
}
[Test]
public void Zufaellige_Werte_zwischen_Minimum_und_Maximum_werden_geliefert() {
Assert.That(sut.GenerateValue(), Is.EqualTo(4));
Assert.That(sut.GenerateValue(), Is.EqualTo(5));
...
}
}
Die Testmethode ist hier verkürzt dargestellt. Ich rufe im Test so lange Werte ab, bis alle möglichen Werte innerhalb von Minimum und Maximum mindestens einmal geliefert wurden.
Der Trick, dass sich der Random-Generator immer gleich verhält, liegt darin, dass ich ihn im Test immer mit demselben Startwert (Seed) 0 instanziere.
Um das zu ermöglichen, habe ich einen internal-Konstruktor ergänzt, der nur im Test verwendet wird, um den Random-Ge-nerator in die Klasse zu injizieren. Der öffentliche Konstruktor der Klasse instan-ziert den Random-Generator ohne Seed, sodass dieser zufällige Werte liefert, siehe Listing 8.
Listing 8: Zufallsgenerator für int-Werte.
public RandomIntGenerator(int minimum, int maximum)
: this(minimum, maximum, new Random()) {
}
internal RandomIntGenerator(int minimum, int maximum, Random random) {
this.minimum = minimum;
this.maximum = maximum;
this.random = random;
}
Bei Konstruktoren sollte man übrigens generell das Highlander-Prinzip beachten: Es kann nur einen geben (eine Anspielung auf den Film Highlander-Es kann nur einen geben). Der interne Konstruktor ist derjenige, der die eigentliche Arbeit verrichtet. Der öffentliche Konstruktor verfügt nur über die beiden Parameter für Minimum und Maximum. Er bezieht sich auf den internen Konstruktor und übergibt diesem, neben den beiden Grenzwerten, auch einen mit new Random() erzeugten Ran-dom-Generator. Das Highlander-Prinzip sollte beachtet werden, damit es in den Konstruktoren nicht zur Verletzung des Prinzips Don't Repeat Yourself (DRY) kommt. Der öffentliche Konstruktor könnte ja die Grenzwerte selbst an die Felder zuweisen, dann würden diese Zuweisungen jedoch an zwei Stellen auftreten.
Eine weitere interessante Implementierung bietet der RollingIntGenerator. Er liefert, ausgehend von einem Minimumwert, immer den nächstenWert, bis er beim Maximumwert angekommen ist. Dann wird wieder von vorn begonnen. Bei diesem Generator lag die Herausforderung darin, korrekt mit dem größtmöglichen int-Wert (int.MaxValue) umzugehen. Ohne UnitTests wäre das ein elendiges Rumprobieren geworden. So war es ganz leicht.
[Abb. 8] Das fertige Portal.
Für Stringwerte habe ich einen Generator implementiert, der eine Liste von Strings erhält und daraus zufällig einen auswählt. Die zur Verfügung stehenden Strings habe ich im Konstruktor als Para-meter-Array definiert, siehe Listing 9.
Listing 9: Zufällig einen Stringwert auswählen.
internal RandomSelectedStringsGenerator(Random random, params string[] values) {
this.random = random;
this.values = values;
}
Das ist für die Unit-Tests ganz angenehm, weil man einfach eine beliebige Liste von Stringwerten übergeben kann:
sut = new RandomSelectedStringsGenerator(
new Random(0), "Apfel", "Birne",
"Pflaume");
Bei der Verwendung des Generators aus Sicht einer Benutzerschnittstelle ist es wünschenswert, einen String zu übergeben, der eine Liste von Werten enthält, die mit Semikolon getrennt sind:
"Apfel; Birne; Pflaume"
Um das zu ermöglichen, habe ich eine separate Extension Method ToValues() implementiert, die einen String entsprechend zerlegt. Diese Methode kann bei Bedarf in den Konstruktoraufruf eingesetzt werden:
"Apfel; Birne; Pflaume".ToValues().ToArray()
Natürlich hätte ich das Zerlegen des Strings in die Einzelwerte auch im entsprechenden Generator implementieren können. Dann hätte der sich aber um mehr als eineVerantwortlichkeit gekümmert. Ferner war die Implementierung so etwas einfacher, da ich mich jeweils auf eine einzelne Aufgabenstellung konzentrieren konnte.
Portal
Das Portal hatte es in sich. Obwohl ich mit WPF schon einiges gemacht habe, fühlte ich mich etwas unsicher, diese sehr dynamische Aufgabenstellung mitWPF anzugehen, und entschied mich daher, das Problem mit Windows Forms zu lösen, weil mir das schneller von der Hand geht. Doch der Reihe nach. Wie die Benutzerschnittstelle des Testdatengenerators ungefähr aussehen könnte, habe ich in der Aufgabenstellung bereits durch ein Mockup angedeutet. Abbildung 8 zeigt, wie mein Ergebnis aussieht.
[Abb. 8] Das fertige Portal.
Ich sehe beim Portal zwei Herausforderungen: Die Anzahl der Spalten in den zu generierenden Daten ist variabel. Daraus ergibt sich, dass die Anzahl der Controls für Spaltendefinitionen variabel sein muss. Im Mockup habe ich daher Schaltflächen vorgesehen, mit denen eine Spaltendefinition entfernt bzw. hinzugefügt werden kann.
Die zweite Herausforderung sehe ich im Aufbau der Spaltendefinitionen. Je nach ausgewähltem Generatortyp sind unterschiedliche Eingaben notwendig. Mal sind zwei Textfelder für Minimum und Maximum erforderlich, mal nur eine für die Elemente einer Liste. Das heißt, dass sich der Aufbau der Benutzeroberfläche mit der Wahl des Generatortyps ändert.
Um diese beiden Herausforderungen möglichst isoliert angehen zu können, habe ich für die variablen Anteile einer Spaltendefinition mit UserControls gearbeitet. So habe ich für einen Generator, der Minimum- und Maximumwerte benötigt, ein UserControl erstellt, in dem zwei Textboxen mit zugehörigen Labels zusammengefasst sind.
Wird aus der Dropdownliste ein Generator ausgewählt, muss das zum Generator passende Control angezeigt werden. Ferner muss zum ausgewählten Generator später die zugehörige ColumnDefinition erzeugt werden, um damit dann die Daten zu generieren. Diese Informationen habe ich im Portal in einer Datenklasse Spalten-Definition zusammengefasst. Objekte dieser Klasse werden direkt in der Dropdownliste verwendet. Daher enthält die Spalten-Definition auch eine Beschreibung. Diese wird als DisplayMember in der Dropdownliste angezeigt, siehe Listing 10.
Listing 10: Eine Spalte definieren.
public class SpaltenDefinition {
public string Bezeichnung { get; set; }
public Type ControlType { get; set; }