Zum Jahresende: Koordinierte Weltzeit und irdisches Schwanken
0 Kommentare Eingestellt von Sebastian Jancke um 16:51Heute morgen las ich in der Süddeutschen Zeitung folgende sehr schöne Formulierung, die in ihrer Wort- und vor allem Namenswahl selten ist:
Dies erinnert mich an den schönen Werbespot von Monster.de, indem Menschen mit sehr langen Beinen die Erd-Rotation mit einer Art Fahrrad sicherstellen ;-)
Zum vierten Mal ein ALT.NET Bier im schönen Köln, diesmal zum Thema Buildmanagement (mit Maven). Stefan, Sergey, Lars und ich hatten dazu Christian Raschka eingeladen. Nach einem kleinen “warm-up” gings ab 19h mit einer Einführung in Maven los. Anschließend diskutierten wir den aktuellen Stand des NPanday-Projektes (früher: NMaven).
Maven ist ein java-basiertes System zum Buildmanagement. Maven ist dabei nur eine sehr kleine Laufzeitumgebung, die erst durch die große Zahl verschiedenster Plugins zum Leben erwacht und arbeitet. Interessant für ALT.NET sind dabei vor allem die Konzeote und Ideen – auch wenn in gemischten Umgebungen ein java-basiertes Buildsystem durchaus pragmatisch erscheint.
Während Ant, NAnt und MSBuild als imperativ aufgefasst werden können, ist Maven rein deklarativ: Man beschreibt was man möchte (Abhängigkeiten, Compiler, Testrunner, Analysen, Reports, …) und Maven kümmert sich um den Rest. Grundsätzlich gilt hier “Convention-over-Configuration”. Es gibt für alles sinnvolle Standardwerte. Zusätzlich sind bestimmte “best practices” als feste Regeln in Maven verbaut. So ist zum Beispiel die Struktur eines Moduls/Projektes immer gleich. Dies hat den Vorteil, dass man darüber nicht mehr nachdenken muss, alle Projekte gleich aussehen und der Standard bereits erprobt ist. Die verschiedenen Bestandteile eines “build” (Phasen!) sind in Maven fest verbaut und entstehen nicht – wie bei Ant – durch das aneinanderreihen von “targets”. Der Vorteil: Eine Phase heißt überall auf der Welt gleich und nicht einmal install, einmal compile und einmal build – hier greifen auch die angesprochenen best practices. Zusätzlich erledigt eine Phase immer die selbe Aufgabe, egal in welchen Projekt (Phasen können trotzdem erweitert werden).
Eine Assembly (oder in Java: JAR) wird in Maven auch Artefakt genannt. Ein Artefakt kann Abhängigkeiten zu anderen, eigenen Modulen haben oder aber auch externe Abhängigkeiten.
Alle Plugins werden genauso wie Abhängigkeiten zu anderen Modulen aufgefasst: Alles ist eine Abhängigkeit des Buildprozesses. Fehlt ein Plugin oder eine Abhängigkeit, so wird dieses von zentralen Servern (sog. “Repositories”) heruntergeladen. Dort können wiederum weitere (transitive) Abhängigkeiten hinterlegt sein; diese werden dann auch geladen. Alle Artefakte werden in einem lokalen Repository abgelegt, dieses funktioniert also als Cache. Für Unternehmen lohnt sich der Einsatz eines Proxy-Repositories im Unternehmensnetzwerk. Dieser Proxy hat seinen eigenen Cache und versucht Anfragen zunächst daraus zu bedienen. Fehlen hier Artefakte, werden diese über konfigurierbare zentrale Repositories heruntergeladen.
Maven vergibt für erstellte Artefakte automatisch Buildnummern, die sich niemals doppeln können. Grundsätzlich ist jeder Build zunächst als sog. “Snapshot” mit dem jeweiligen System-Benutzernamen markiert. Erst ein spezielles Plugin erzeugt auf Wunsche in komplettes Release. Zur Erzeugung eines solchen gehört dann auch der Upload auf ein angegebenes Repository. Auf öffentlichen zentralen Repositories sind idR keine Snapshots zu finden, dies garantiert die Stabilität der veröffentlichen Versionen. Wer dennoch gegen den “trunk/HEAD” bauen möchte, kann meist Snapshot-Versionen von den Servern der jeweiligen Projekte bekommen. Da Abhängigkeiten mit Versionsnummern definiert werden sollten (oder mit Versionsbereichen), erreicht Maven somit die so wichtige Reproduzierbarkeit von Builds.
Alle beschriebenen Abhängigkeiten eines Artefakts werdem mit dem Project Object Model (POM) beschrieben. Dazu können auch Angaben über das verwendete SCM (mit Pfad zum Checkout) oder ein Ziel-Repository für Releases gehören. Fehlt der Quellcode kann Maven diesen automatisch aus dem gegebenen SCM herunterladen. Wird ein Release erzeugt, wird zunächst kontrolliert, ob im SCM noch Änderungen vorhanden sind. Falls es solche gibt, werden diese zunächst auch “ausgecheckt”. Anschließend wird der komplette Build durchgeführt und der Stand im SCM getagged. Das fertige Release wird dann ins lokale Repository und in das angegebene Ziel-Repository installiert.
Für .NET gibt es schon länger ein Reihe von Maven-Plugins, um .NET Compiler, Testrunner, Analysen, etc. zu unterstützen. Nach 2 Jahren im Apache Incubator konnte das Projekt aber nicht genügend Geschwindigkeit und Halt in der Community erzeugen. Deshalb ist der aktuelle (funktionierende) Stand nun unter neuem Namen nach Codeplex gewandert. Die Plugin-Sammlung heißt nun NPanday. Damit ist Maven sicherlich näher an .NET gerückt, fraglich ist aber ob der Ansatz einer Java-Runtime beibehalten werden kann. Derzeit scheinen die Pläne vorzusehen, sich noch stärker mit MSBuild im Rücken zu verbinden und langfristig die Java-Abhängigkeit zu verlieren. Auch veröffentlichen derzeit keine der großen Open Source Projekte in der .NET Welt ihre Artefakte auf zentralen Repositories. Für mehr Sichtbarkeit von Maven und die Aufnahme von mehr Geschwindigkeit müssten sicherlich Projekte wie NHibernate, Castle Windsor, Spring.NET, NUnit, xUnit, … sich beteiligen und ein Deployment auf einem zentralen Server pflegen. Dazu müsste man sicherlich “unten” anfangen und die Basis (Unittesting-Frameworks, Castle Dynamic Proxy) zuerst deployen, damit alle darauf aufsetzenden Projekte ihre Abhängigkeiten korrekt definieren können.
Vor ca. zwei Wochen habe ich mit dem ASP.NET MVC Framework im Rahmen eines verteilten Architektur-Prototypen begonnen. Ich war ich natürlich gespannt, ob sich mit dem MVC Framework auch nach BDD entwickeln lässt. Dies ist schließlich das Versprechen des MVC-Teams: Testbarkeit.
Ich kann sagen, dass nicht zu viel versprochen worden ist. Die Arbeit mit dem MVC Framework und die Erstellung einer Web-UI damit lässt sich problemlos mit Behavior Driven Development durchführen. Nachdem Albert schon einige seiner Erweiterungen vorgestellt hat, möchte ich mich dem anschließen. Davon abgesehen ist dies ein guter Zeitpunkt, einmal Top-Down einen Controller mittels BDD zu entwickeln und damit BDD an einem konkreten Beispiel zu demonstrieren.
Eingesetzte Frameworks
Zunächst einmal ist es wohl fast Pflicht das Projekt MvcContrib herunterzuladen, da es viele “fehlende” Erweiterungen mitbringt. Fehlend ist dabei in Anführungszeichen, weil sich das MVC Framework durch sehr gute Erweiterbarkeit auszeichnet: Factories für Controller, Routen und auch die komplette Render-Engine lassen sich austauschen. MvcContrib bringt eine Reihe von fertigen Implementierungen für IoC-Container (Castle Windsor, Spring.NET, …) und auch Render-Engines (Brails, NHaml, …) mit. Als Render-Engine setzte ich ASP.NET ein, Windsor ist mein IoC Container der Wahl.
Darüber hinaus kommt für Unit- und Integrationstests xUnit mit Björns xUnit.BDDExtensions zum Einsatz.
Entwicklung eines Controllers anhand einer Userstory
Für diesen Artikel habe ich eine Userstory gekürzt: Wenn der Anwender die Webseite “Index” aufruft, soll ihm “alle” gespeicherten Produkte auf der View “Index” angezeigt werden.
Zunächst erstellen wir also eine Specification für diese Userstory. Hier empfiehlt es sich Björns Templates für den Resharper zu installieren. Eine Specification spiegelt immer genau einen Kontext einer Userstory wieder. Diese Specification erzeugt einen ShopController (zunächst ohne weiteren Kontext). Bisher existiert der ShopController nicht. Diesen legen wir dann an. Damit haben wir in unserer Specification vorerst alles arrangiert (der Arrange-Teil von AAA). Wir können die Specification nun ausführen. Nun fügen wir Verhalten hinzu (der Act-Teil von AAA): die Aktion “Index” wird aufgerufen. Diese aufgerufene Methode des System-Under-Test fügen wir nun in die Klasse ShopController ein. Nun kompiliert die Specification wieder und wir können diese wiederum ausführen. Nun ist es an der Zeit, Beobachtungen einzufügen (der Assert-Teil von AAA). Nach der Erstellung einer Beobachtung in der Specification führen wir diese aus (Resultat: Failure, Rot), danach implementieren wir soviel, bis die Specification wieder grün wird. Anschließend fügen wir inkrementell weitere Beobachtungen ein und erfüllen diese wie beschrieben.
Im optimalen Fall beschreibt eine Userstory immer genau einen Kontext, andernfalls müsste diese noch feiner zerlegt werden. Eine Specification entspricht wie bereits gesagt genau einem Kontext, der sich im Namen der Specification samt Beschreibung der Handlung widerspiegelt. Unüblich für C# und Ähnliche ist die Verwendung von Unterstrichen anstelle von Camel Casing. Trotzdem sollte dieser Stil genutzt werden, da er die Lesbarkeit auch in den Testrunnern und Methoden-Übersichten deutlich erhöht.
Bei der Erstellung des Kontext werden auch alle Abhängigkeiten initialisiert. Dabei versteckt die Basisklasse für Specifications die Mechanik (Mocking Framework, …) zur Erzeugung von Abhängigkeiten. Speziell für das MVC Framework habe ich die Basisklasse “ControllerInstanceContextSpecification” angelegt, da diese die Testhelper von MvcContrib versteckt, um einen Controller vollständig zu initialisieren.
Als Beobachtungen sind grundsätzlich Überprüfung des Ergebnisses oder Überprüfung von Methodenaufrufen und Exceptions denkbar. Für Ergebnisse werden diese nach der Handlung in einem privaten Feld der Specification gespeichert (hier: “result”). Für die Überprüfung von Methodenaufrufen und Exceptions ist ein Mocking Framework nötig, das den AAA-Stil unterstützt (zB Rhino.Mocks). Ältere Record-Reply Varianten funktionieren damit nicht. Zur erhöhten Lesbarkeit werden Assertions auf Werten und Methodenaufrufen hinter besser lesbaren Extension-Methods versteckt. Beobachtungen erfüllen somit auch die Forderung, pro “Test” nur eine Assertion auszuführen – andernfalls sind Fehlschläge in Tests schwerer zu lokalisieren.
Der beschriebene Quelltext
Hier nun die komplette Specification:
1: [Concern(typeof (ShopController))]
2: public class when_a_shop_controller_handles_the_index_action :
3: ControllerInstanceContextSpecification<ShopController>
4: {
5: private IDiscService discService;
6: private ActionResult result;
7:
8: protected override void EstablishContext()
9: {
10: discService = Dependency<IDiscService>();
11:
12: discService.WhenToldTo(x => x.FindDiscsForSale())
13: .Return(new List<Disc>
14: {
15: new Disc(3698, "U2 / All the best",
16: 2003,
17: "classical"),
18: });
19: }
20:
21: protected override ShopController CreateSut()
22: {
23: return new ShopController(discService);
24: }
25:
26: protected override void Because()
27: {
28: result = Sut.Index();
29: }
30:
31: [Observation]
32: public void should_redirect_to_search_view()
33: {
34: result.should_be_rendered_view("Index");
35: }
36:
37: [Observation]
38: public void should_retrieve_discs_from_disc_service()
39: {
40: discService.AssertWasCalled(x => x.FindDiscsForSale());
41: }
42: }
Die Implementierung des Controllers sieht dann wie folgt aus:
1: [HandleError]
2: public class ShopController : Controller
3: {
4: private readonly IDiscService service;
5:
6: public ShopController(IDiscService service)
7: {
8: this.service = service;
9: }
10:
11: public ActionResult Index()
12: {
13: /* ...
14: */
15:
16: var model = new ShopViewModel();
17: model.Discs = service.FindDiscsForSale();
18: return View("Index", model);
19: }
20: }
Zu guter letzt ist noch die neue Basisklasse für Instance-Specifications nötig. Diese ist speziell für Controller des MVC Frameworks und versteckt die Mechanik der Testhelper von MvcContrib bei der Initialisierung eines Controllers.
1: public abstract class ControllerInstanceContextSpecification<T> : InstanceContextSpecification<T>
2: where T : Controller
3: {
4: protected override void InitializeSystemUnderTest()
5: {
6: base.InitializeSystemUnderTest();
7: new TestControllerBuilder().InitializeController(Sut);
8: }
9: }