Design Patterns: Teil 3 - Testbarkeit

Von Andreas Aschauer Autor Feed 11. January 2010 11:51

Ziel dieser Blogreihe soll es nicht sein Pattern A bis Z vorzustellen (dafür gibt es genügend Bücher) sondern praktische Tutorials zu liefern, wie man Design Patterns und Entwicklungsmethodiken einsetzen kann um sich das Entwicklerleben zu erleichtern und die Qualität des Codes zu verbessern.

In den vorangegangen Artikeln der Design Patterns Reihe haben wir schrittweise eine modulare und lose gekoppelte Architektur entwickelt. Am Beispiel des LogWriters war schön zu erkennen, wie einfach und schnell man Modularisierung und lose Kopplung auch in scheinbar kleinen Anwendungen (die ja meist dann zu unternehmenskritischer Software heranwachsen) erreichen kann. Jetzt gehen wir einen Schritt weiter und überlegen wozu man das ganze verwenden kann. Eine gute Architektur sollte im Entwicklungsprozess auch so gut als möglich “ausgenutzt” werden, denn die Architektur allein schafft noch keinen hochqualitativen Entwicklungsprozess. Der erste Schritt dazu sind Entwicklertests! Damit ist nicht Debugging gemeint sondern eine Testgetriebene-Entwicklung, die die Brücke zwischen funktionierendem Code und Code, der funktioniert UND der Spezifikation entspricht, schlägt. Test-Driven-Development (TDD), das MVC Framework für ASP.NET und die hier vorgestellten Tools werden noch genauer in Folgeartikeln behandelt, vorerst wollen wir anhand eines kurzen Beispiels mittels ASP.NET MVC ansehen, wie leicht man Unit-Tests in einer gut strukturierten Anwendung unterbringen kann. Bis dahin sei auf die Links unten verwiesen.

Erstellt man in Visual Studio ein neues MVC Projekt aus der Vorlage “ASP.NET MVC 2 Web Application”, welche via Download des ASP.NET MVC Frameworks mitgeliefert wird, wird man anfangs sofort gefragt, ob man seiner neuen Solution gleich Unit Tests hinzufügen möchte – wollen wir natürlich!

image

Im Anhang ist der Source Code einer Minianwendung zu finden, die nichts anderes tut als eine Eingabemaske zur Verfügung zu stellen um einen Kunden neu anzulegen.

Was wollen wir erreichen? Wir wollen nun testen ob beim Anlegen eines Kundendatensatzes die Muss-Felder geprüft werden und entsprechende Fehlermeldungen erscheinen. Was wir nicht wollen, ist das bei jedem Testdurchlauf Daten in die Datenbank geschrieben werden – 1. dauert das lange wenn viele Tests laufen und 2. müsste bei komplexeren Szenarien jedesmal die Datenbank zurückgesetzt werden. Genau hier kommt unsere Architektur wie wir sie im letzten Artikel erarbeitet haben zum Einsatz.

Dazu implementieren wir den CustomerController folgendermassen:

   1: private readonly ICustomerRepository _customerRepository;
   2:  
   3: public CustomerController(ICustomerRepository customerRepository)
   4: {
   5:     _customerRepository = customerRepository;
   6: }
   7:  
   8: public ActionResult Create(Customer customer)
   9: {
  10:     if(string.IsNullOrEmpty(customer.FirstName))
  11:     {
  12:         ModelState.AddModelError("FirstName","Firstname must not be empty!");
  13:     }
  14:     _customerRepository.Save(customer);
  15:     return View();
  16: }

Wichtig ist hierbei nur die Tatsache, dass die Abhängigkeit des Controllers vom CustomeRepository, dass das eigentlich Speichern übernimmt injeziiert wird.

Das ermöglicht uns dass wir ein “gefaktes” CustomerRepository implementieren, also eine Klasse “CustomerRepositoryMock” die einfach ICustomerRepository implementiert und die Methode Save() so defniert wie wir sie zu Testzwecken wollen. Nämlich entweder komplett ohne Funktion oder es könnte auch eine In-Memory Datenbank wie SQLite verwendet werden. Die Implementierung ist im anghängten Sourcecode ersichtlich um den Artikel kurz zu halten. Im Prinzip folgen ICustomerRepository, CustomerRepository und CustomerRepositoryMock dem gleichen Prinzip wie die LogWriter im vorigen Artikel.

Das Schöne ist nun, dass die Methode Create() des CustomerControllers einfach getestet werden kann und GANZ WICHTIG (!!) sie muss NICHT geändert werden um produktiv zu funktionieren! Es muss nur die injeziierte Instanz von ICustomerRepository zB via Windsor Container umgestellt werden

-> siehe Design Patterns Teil 2

Der Test sieht dann so aus:

   1: [TestMethod]
   2: public void ValidationMessagesShouldAppearOnCreate()
   3: {
   4:     var custController = new CustomerController(
   5:     new CustomerRepositoryMock());
   6:  
   7:     var customer = new Customer()
   8:                                {FirstName = ""};
   9:  
  10:     custController.Create(customer);
  11:  
  12:     Assert.AreEqual(1, custController.ModelState.Count);
  13:     Assert.AreEqual("Firstname must not be empty!", custController.ModelState["FirstName"].Errors[0].ErrorMessage);
  14:  
  15: }

Die 2 Assert() Anweisungen prüfen, ob im ModelState der Key “FirstName” vorhanden ist und ob der Eintrag die richtige Fehlermeldung enthält.

image

Hier ist auch schön zu sehen, wie einfach die UI im ASP.NET MVC Framework getestet werden kann.

Eine Einführung ins MVC Framework und noch viel mehr zu TDD und Unit Testing folgen in den nächsten Artikeln!

Links/Downloads:

Beispiel Source Code

ASP.NET MVC Framework

Einführung in TDD

Comments (2) -

>

1/11/2010 6:23:01 PM #

Die Artikelserie liest sich gut. Zu Castle-Windsor-2.0 fehlt mir noch eine Begründung, warum ich mir diese Abhängigkeit in mein Projekt einbauen sollten(Stabilität, Support, etc) Gerade für unternehmenskritische Anwendungen nicht ganz unumstritten. Welche Alternative hätte ich zu Castle-Windsor-2.0 - wie könnte man das ganze noch lösen.
Ansonsten bin ich gespannt, wie es weiter geht.

Rene

Rene Drescher-Hackel Germany

>

1/11/2010 8:06:12 PM #

Der Grund ein IoC Framework wie Castle Windsor (oder ein anderes, siehe Teil 2 der Serie unter Links) einzusetzen liegt darin, dass es die Abhängigkeiten ZUR LAUFZEIT auflöst. Im obigen Beispiel habe ich zwar Abhängigkeiten von aussen injiziiert aber ich habe keine Möglichkeit diese ohne Neukomplilierung zu verändern.
Ein Paradebeispiel hierfür sind externe Services die zur Entwicklungszeit nicht zur Verfügnung stehen. Diese werden in der Enwicklungsumgebung "gemockt" und dann in der Produktivumgebung durch einfaches Anpassen der Konfiguration (Siehe Teil 2) wird dem Container des IoC Frameworks dann mitgeteilt, dass die Interfaces gegen die programmiert wird, jetzt von anderen Klassen (nämlich den echten Services) implementiert werden.
Auch nicht unüblich sind Szenarien in denen sich zum Beispiel Berechnungsalgorithmen ändern während das System schon produktiv ist.
Wird die Berechnung sauber separiert in zB ein Service "A" ausgelagert und in der Anwendung gegen ein Interface programmiert, kann wieder durch Anpassen der Containerkonfiguration einfach auf ein neues Berechnungsservice "B" umgestellt werden.

Ich hoffe damit ist der Einsatz des Frameworks klarer geworden.

Andreas Aschauer Österreich

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading

www.microsoft.com/austria | © 2009 Microsoft Corporation. Alle Rechte vorbehalten.
BlogEngine.NET 2.5.0.6 powered by atwork