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: }