06 Dec
2008

Extension-Methods für ASP.NET MVC Unit-Tests

 

Ich habe ein paar Extension-Methods für schönere Unit-Tests vorgestellt. Nun stelle ich wieder zwei vor. Diese sind speziell für das ASP.NET MVC Framework mit der ASP.NET MVC Futures (Microsoft.Web.Mvc.Dll) Erweiterung.

public static class MvcBDDExtension
{
    public static void should_link_to<T>(this Expression<Action<T>> expected, Expression<Action<T>> action) where T : Controller
    public static void should_route_to<T>(this ActionResult actionResult, Expression<Action<T>> action) where T : Controller
}

Damit kann man überprüfen ob ein typisierter Link zu einer bestimmten Controller-Action gesetzt ist und ob ein RedirectRouteResult zu einer bestimmte Controller-Action zurückgegeben worden ist. Den Quelltext gibt es am Ende.

Erst einmal ein paar erklärende Worte vorneweg.

Typisierte Links sind mit den ASP.NET MVC Futures möglich. Diese haben den Vorteil dass ich die Controller als auch die Actions per Refactoring umbenennen kann ohne das mir die Anwendung kaputt geht. Man kann die Textuelle Verlinkung zwar mit Unit-Tests überprüfen, jedoch sollte ein Umbenennen einer Klasse oder Methode kein Anpassung von Unit-Tests zur Folge haben.

So sieht die Erstellung von typisierten Links innerhalb eines Views aus, am konkreten Beispiel einer MasterPage.

<div id="menucontainer">
    <ul id="menu">
        <li>
            <%=Html.ActionLink(Model.LinkToHome, @"Startseite")%></li>
        <li>
            <%=Html.ActionLink(Model.LinkToAboutUs, @"Impressum")%></li>
    </ul>
</div>

Das ViewModel sieht in diesem Fall so aus.

using System;
using System.Linq.Expressions;

namespace Newsletter.Web.Controllers.ModelViewData
{
    public class ViewDataBase
    {
        public Expression<Action<AccountController>> LinkToLogout;
        public Expression<Action<AdminController>> LinkToAdmin;
        public Expression<Action<AccountController>> LinkToRegister;
        public Expression<Action<HomeController>> LinkToHome;
        public Expression<Action<HomeController>> LinkToAboutUs;
        public string Title { get; set; }
        public string Username { get; set; }
    }
}

Der View ist entsprechend typisiert und bietet eine Model-Property zum einfacheren Zugriff auf das ViewModel.

using System;
using System.Web.Mvc;

using Newsletter.Web.Controllers.ModelViewData;

namespace Newsletter.Web.Views.Shared
{
    public partial class Site : ViewMasterPage<ViewDataBase>
    {
        protected ViewDataBase Model
        {
            get { return ViewData.Model; }
        }
    }
}

Die Controller-Actions werden dann folgendermaßen Eingetragen. Es sind verschiedene Controller mit entsprechenden Actions.

public static class ViewDataExtensions
{
    public static void SetMasterPageData(this ViewDataBase viewData)
    {
        viewData.LinkToAdmin = ac => ac.Index();
        viewData.LinkToRegister = ac => ac.Register(string.Empty, string.Empty, string.Empty, string.Empty);
        viewData.LinkToLogout = ac => ac.Logout();
        viewData.LinkToHome = hc => hc.Index();
        viewData.LinkToAboutUs = hc => hc.About();
    }
}

Damit sind typisierte Links auf Controller-Actions gesetzt und ich brauche mir keine Gedanken zu machen was passiert wenn ich diese umbenenne.

Es ist jedoch erforderlich in einem Unit-Test zu überprüfen ob auch wirklich bei jedem View die entsprechenden Links im ViewModel gesetzt sind.

Hier schlägt nun die Stunde für die should_link_to<T>() Extension-Method.

[Observation]
public void the_MasterPageLinks_should_be_set_if_a_ViewResult_is_given_back()
{
    if (result is ViewResult)
    {
        var model = GetViewModel<ViewDataBase>();

        model.LinkToAboutUs.should_link_to(homeController => homeController.About());
        model.LinkToHome.should_link_to(homeController => homeController.Index());

        model.LinkToAdmin.should_link_to(adminController => adminController.Index());
        model.LinkToLogout.should_link_to(accountController => accountController.Logout());
        model.LinkToRegister.should_link_to(accountController => accountController.Register(string.Empty, string.Empty, string.Empty, string.Empty));
    }
}

Durch einfaches Lesen des Quelltextes kann man nun schnell erkennen was dazu führen muss um den Test zum funktionieren zu bringen.

result ist ein field innerhalb der Test-Klasse welchen den ActionResult der zu testenden Controller-Action beinhaltet und GetViewModel<T>() holte das aktuelle ViewModel aus dem result.

Möchte man jedoch Testen ob eine Weiterleitung zu einer bestimmten Controller-Action vorgenommen wurde. So ist dies mit should_route_to<T>() möglich.

using System;
using System.Web.Mvc;

using DerAlbert.UnitTest.Base.BDD;

using Newsletter.Manager;
using Newsletter.Web.Controllers;
using Newsletter.Web.Controllers.ModelViewData;

namespace Newsletter.Controllers.Tests.SpecsSubscriberController.AddNewSubscriber
{
    [Concern(typeof (SubscriberController))]
    public class When_the_user_request_the_newsletter_on_the_addnew_page : concern_of_SubscriberController
    {
        // einiges an Setup und Observations für das Beispiel weggelassen, 
        // da es sonst zu unübersichtlich als Beispiel wird.

        private AddNewSubscriberViewData viewData;

        protected override void establish_context()
        {
            base.establish_context();

            viewData = new AddNewSubscriberViewData
                           {
                               SubscriberName = @"Hubert",
                               EMail = @"hello@world.de",
                               EMailVerify = @"hello@world.de",
                               Result = @"an Answer"
                           };

            subscriberManager.when_told_to(sm => sm.ValidateCaptcha(viewData)).Return(true);
        }

        protected override void because()
        {
            result = sut.AddNewSubscriber(viewData);
        }
    
        [Observation]
        public void the_page_should_be_redirected_to_SubscriberController_SubscriberAdded()
        {
            result.should_route_to<SubscriberController>(sc => sc.SubscriberAdded());
        }
    }
}

Dies ist ein Real-World BDD-Style Test und hier etwas abgespeckt dargestellt. Hier sieht man im Quelltext was passiert muss damit eine Spezifikation erfüllt ist.

Jetzt ist es soweit der Quelltext der Methoden. Diese brauchen die ASP.NET MVC Future und die vorhandenen BDD Extensions.

Happy Testing mit den Extension-Methods.

using System;
using System.Linq.Expressions;
using System.Web.Routing;

using Microsoft.Web.Mvc.Internal;

using System.Web.Mvc;

namespace DerAlbert.UnitTest.Base.BDD
{
    public static class MvcBDDExtension
    {
        public static void should_link_to<T>(this Expression<Action<T>> expected, Expression<Action<T>> action) where T : Controller
        {
            RouteValueDictionary expectedDictionary = ExpressionHelper.GetRouteValuesFromExpression(expected);
            RouteValueDictionary actionDictionary = ExpressionHelper.GetRouteValuesFromExpression(action);
            actionDictionary.should_only_contain(expectedDictionary);
        }

        public static void should_route_to<T>(this ActionResult actionResult, Expression<Action<T>> action) where T : Controller
        {

            var redirectResult = (RedirectToRouteResult)actionResult;
            RouteValueDictionary actionDictionary = ExpressionHelper.GetRouteValuesFromExpression(action);
            actionDictionary.should_only_contain(redirectResult.Values);
        }
    }
}

Habt Ihr eigene Extension-Methods für Test? Her damit!
Wie haltet Ihr dass mit dem Testen?
Ist dies hier alles Mumpitz und Assert.IsXYZ() is viel besser?

Verehrte Leser, nutzt die Kommentarfunktion.

Technorati-Tags: ,,

Der Eintrag ist mir etwas Wert
 

Feedback

# re: Extension-Methods für ASP.NET MVC Unit-Tests

left by Robert at 12/8/2008 8:40 AM Gravatar
Sehr coole Idee. Die Gefahr die ich dabei sehe ist allerdings, dass man durch diese Extension Methods übersieht was nun "Extension" ist, und was eigentlich zur Klasse gehört.
Wenn z.B. ein neuer Kollege ins Team kommt und er keine Asserts findet, wird er erstmal leicht verwirrt sein.
Hier sollte man vielleicht sich doch noch eine Richtlinie einfallen lassen das deutlich zu machen.

PS: Und Methoden klein schreiben? Wir sind hier doch nicht bei Java ;)

# re: Extension-Methods für ASP.NET MVC Unit-Tests

left by Der Albert at 12/8/2008 11:44 AM Gravatar
Die Schreibweise ja nur bei den Test so, so sieht man direkt was zum Test gehört und nicht zur Klasse.

Wer sich lange davon verwirrent lässt das odrt kein Assert mehr steht, der sollte aufhören Software zu schreiben.

Extension-Methods können auch farblich anders gekennzeichnet werden.

# re: Extension-Methods für ASP.NET MVC Unit-Tests

left by Robert at 12/8/2008 2:35 PM Gravatar
Wenn es nicht deutlich ist, was eigentlich der Test nun ist, dann ist das durchaus für spätere Teammitglieder hinderlich.
Vielleicht gibt es ja tatsächlich irgendwelche Leute die in ihrere BuisnessLogic irgendwelche Methoden namens "ShouldBlablabla" haben.
Wenn man dies jetzt allerdings nicht auf den ersten Blick sieht, dann ist das etwas ungünstig.

Wenn ein UnitTest nicht geht, dann sollte der Name der Methode mir schon relativ viel verraten. Dann brauche ich momentan nur auf die Asserts schauen und sehe, was gefordert ist.
Es kann durch solche Extension Methods IMHO auch undeutlicher werden.
Zudem wüsste ich jetzt auch nicht, wie ich die Extension Methods andersfarbig mache ;)

# re: Extension-Methods für ASP.NET MVC Unit-Tests

left by Der Albert at 12/8/2008 2:42 PM Gravatar
Dies ist aber ein generelles Problem von Extension Methods das man nicht auf Anhieb erkennen kann ob es zur Klasse gehört oder nur "angeflanscht" ist.

Beim BDD geben die Namen der Tests die fachliche Spezifikation vor. Die Asserts bzw. diese EMs prüfen dies auf der technischen Ebene.

Zur Farbigkeit.

Visual Studio => Tools => Options => Fonts and Colors => Display items => ReSharper Extension Method Identifier => Farbe der Wahl einstellen.

Ok, dafür ist ReSharper notwendig ;)
Comments have been closed on this topic.