2010-04-26 11 views
20

Ich bin Gerät testen meine Routen in ASP.NET MVC 2. Ich benutze MSTest und ich verwende auch Bereiche.Unit testet ASP.NET MVC 2 Routen mit Bereichen ausbucht auf AreaRegistration.RegisterAllAreas()

[TestClass] 
public class RouteRegistrarTests 
{ 
    [ClassInitialize] 
    public static void ClassInitialize(TestContext testContext) 
    { 
     RouteTable.Routes.Clear(); 

     RouteTable.Routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 
     RouteTable.Routes.IgnoreRoute("{*favicon}", new { favicon = @"(.*/)?favicon.ico(/.*)?" }); 

     AreaRegistration.RegisterAllAreas(); 

     routes.MapRoute(
      "default", 
      "{controller}/{action}/{id}", 
      new { controller = "Home", action = "Index", id = UrlParameter.Optional } 
     ); 
    } 

    [TestMethod] 
    public void RouteMaps_VerifyMappings_Match() 
    { 
     "~/".Route().ShouldMapTo<HomeController>(n => n.Index()); 
    } 
} 

Wenn es AreaRegistration.RegisterAllAreas() jedoch ausgeführt wird, wirft sie diese Ausnahme:

System.InvalidOperationException: System.InvalidOperationException: Diese Methode kann nicht während der Anwendung vor dem Start Initialisierungsphase aufgerufen werden.

Also, ich denke, ich kann es nicht von meinem Klasseninitialisierer aufrufen. Aber wenn kann ich es nennen? Ich habe natürlich keinen Application_Start in meinem Test.

+0

Was ist der Zweck des Parameters 'testContext'? Es wird nie benutzt. – MEMark

+0

@MEMark - es ist erforderlich für [ClassInitialize] – Swati

+0

@Swati Natürlich. Ich habe MSTest schon lange nicht mehr benutzt. – MEMark

Antwort

29

Ich löste dies durch eine Instanz meiner AreaRegistration Klasse zu schaffen und die RegisterArea Methode aufrufen.

Zum Beispiel bei einem gegebenen Bereich mit dem Namen "Katalog" mit dieser Route:

public override void RegisterArea(AreaRegistrationContext context) 
{ 
    context.MapRoute(
     "Catalog_default", 
     "Catalog/{controller}/{action}/{id}", 
     new {controller = "List", action = "Index", id = "" } 
); 
} 

Dies ist mein Test-Methode:

[TestMethod] 
public void TestCatalogAreaRoute() 
{ 
    var routes = new RouteCollection(); 

    // Get my AreaRegistration class 
    var areaRegistration = new CatalogAreaRegistration(); 
    Assert.AreEqual("Catalog", areaRegistration.AreaName); 

    // Get an AreaRegistrationContext for my class. Give it an empty RouteCollection 
    var areaRegistrationContext = new AreaRegistrationContext(areaRegistration.AreaName, routes); 
    areaRegistration.RegisterArea(areaRegistrationContext); 

    // Mock up an HttpContext object with my test path (using Moq) 
    var context = new Mock<HttpContextBase>(); 
    context.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath).Returns("~/Catalog"); 

    // Get the RouteData based on the HttpContext 
    var routeData = routes.GetRouteData(context.Object); 

    Assert.IsNotNull(routeData, "Should have found the route"); 
    Assert.AreEqual("Catalog", routeData.DataTokens["area"]); 
    Assert.AreEqual("List", routeData.Values["controller"]); 
    Assert.AreEqual("Index", routeData.Values["action"]); 
    Assert.AreEqual("", routeData.Values["id"]); 
} 
+0

Ich denke, das ist eine gute Lösung, danke. –

+0

Ich konnte das nicht mit meinem 'Administration' Bereich arbeiten. routeData gibt null zurück. Könnte MVC 4 anders sein, um dies zu verursachen? Ich komme ein wenig weiter mit meiner ursprünglichen Testvorrichtung, die sehr ähnlich ist, aber es passt nicht auf die Gebietsroute und fällt stattdessen auf die Standardroute. –

+0

Aha! Habe es herausgefunden ... Ich habe den Standard-Controller nicht eingestellt, als ich meinen Bereich registriert habe (was der Standard der Vorlage ist). Plus 1. Danke! –

10

Nun, es gibt keinen Platz im Test-Projekt können Sie AreaRegistration.RegisterAllAreas(); damit es funktioniert, weil es die Klasse System.Web.Compilation.BuildManager verwendet, um Code für die Website zu kompilieren, und das fehlschlägt, wenn es außerhalb der ASP.NET-Pipeline aufgerufen wird. Ich denke, es ist eine Art Bug, weil es Tests wirklich sehr schwierig macht.

Aber ich habe eine 2-Schritt Abhilfe erfunden :)

Zuerst sollten Sie App.Config Datei Ihres Testprojekt

<?xml version="1.0" encoding="utf-8" ?> 
<configuration> 
    <appSettings> 

    </appSettings> 

    <connectionStrings> 

    </connectionStrings> 
    <system.web> 
     <compilation debug="true"> 
      <assemblies> 
       <add assembly="!!!NAME_OF_YOUR_MVC_WEB_ASSEMBLY!!!"/>  
      </assemblies> 
     </compilation> 
    </system.web> 
    </configuration> 

Actualy Sie refference alle Baugruppen, die enthält AreaRegistration Unterlängen ändern sollte. Zweitens fügen Sie diesen hässlichen Code vor AreaRegistration.RegisterAllAreas();

typeof(BuildManager).GetProperty("PreStartInitStage", BindingFlags.NonPublic | BindingFlags.Static).SetValue(null, 2, null); 

typeof(BuildManager).GetField("_topLevelFilesCompiledStarted", BindingFlags.NonPublic | BindingFlags.Instance).SetValue( typeof(BuildManager).GetField("_theBuildManager", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null), true); 

Dies funktioniert nur für .NET 4.0 und höher

+0

Danke. Obwohl es nicht sehr hübsch ist, macht es meinen Test bestanden. Ich habe festgestellt, dass es beim Debuggen des Tests fehlschlägt und eine andere Ausnahme auslöst (etwas über einen Dateinamen, der ein unzulässiges Zeichen enthält). Wie auch immer, ich werde deine Antwort für jetzt akzeptieren. Ich hoffe immer noch auf eine richtige Lösung. –

+0

Woks einfach gut! Was für ein hässlicher Hack, aber ... –

+0

Lifesaver, Prost! –

1

Ich glaube, Sie suchen die TestHelper Klasse in der MVC Contrib Bibliothek. Sehen Sie sich die Tests in MVC Contrib an (es ist dort versteckt). Sie werden feststellen, dass alles schön verspottet ist. H

MVCContrib.UnitTests\TestHelper\RoutesTest.cs - muss das Wiki aktualisieren! Viel Glück

using System.Web.Mvc; 
using System.Web.Routing; 
using NUnit.Framework; 

namespace MVCContrib.Application.UnitTests.TestHelper 
{ 
    /// <summary> 
    /// Summary description for UserRoutesTest 
    /// </summary> 
    [TestFixture] 
    public class UserRoutesTest 
    { 
     [TestFixtureSetUp] 
     public void Setup() 
     { 
      var routes = RouteTable.Routes; 
      routes.Clear(); 
      routes.MapRoute(
       "Default",            // Route name 
       "{controller}",           // URL with parameters 
       new { controller = "Home", action = "Index", id = "" } // Parameter defaults 
       ); 

     } 

     [Test] 
     public void homeIndex() 
     { 
      "~/user" 
       .ShouldMapTo<HomeController>(action => action.Index()); 
     } 


     [Test] 
     public void HomeShow() 
     { 
         "~/home" 
          .GivenIncomingAs(HttpVerbs.Put) 
          .ShouldMapTo<HomeController>(action => action.Index()); 
     } 

    } 
} 
+1

Wie Sie in meinem Codebeispiel sehen können, verwende ich bereits den MvcContrib-Testhelfer (Aufruf der 'ShouldenMapTo'-Erweiterungsmethode). Es scheint derzeit keine spezifische Unterstützung für Bereiche in MvcContrib zu geben, oder sie haben es nicht offensichtlich gemacht. Selbst wenn es genügend Unterstützung für Testbereiche gibt, würde ich immer noch das Problem haben, dass mein Routenregistrierungscode diese Ausnahme auslöst, wenn er von einem Komponententest aufgrund des 'AreaRegistration.RegisterAllAreas()' Aufrufs aufgerufen wird. –

11

Ich weiß, ich bin spät läuten hier, aber ich habe gerade selbst dieses Problem gelöst. Eine ähnliche Lösung wie Jason (registriere einen Bereich nach dem anderen), aber wie du benutze ich MvcContrib.TestHelper, anstatt mich selbst zu verspotten.

Beachten Sie, dass MvcContrib eine harte Abhängigkeit von Rhino Mocks hat. Während ich es vorziehe, Moq zu verwenden, bin ich mit der Rhino-DLL einverstanden, nur um diese nette Funktionalität zu erhalten.

+0

+1 Nie zu spät für eine gute Antwort – StarTrekRedneck

+1

Leider hat MVCContrib keine Unterstützung für MVC 4 oder MVC 5, es sei denn, Sie kompilieren es selbst. Ich erhalte einen Fehler, weil der Typ des Controllers nicht mit dem übereinstimmt, mit dem er in ShouldMapTo () kompiliert wurde. Danke, aber im Moment ist das zu viel Aufwand. – NightOwl888

2

Hier eine schöne Version mit kombinierten Ansätzen.

-Code verwendet von:


[TestClass] 
public class RoutesTest : RoutesTestClassBase<SomeAreaRegistration> 
{ 
    [TestMethod] 
    public void IdWithoutName() 
    { 
     // Area-Name is retrieved from the Registration 
     // and prepended as "~/AreaName/" 

     TestRoute("Contacts/Show/0627ED05-BF19-4090-91FC-AD3865B40983", new { 
      controller = "Contacts", 
      action = "Show", 
      id = "0627ED05-BF19-4090-91FC-AD3865B40983" 
     }); 
    } 

    [TestMethod] 
    public void IdAndName() 
    { 
     TestRoute("Contacts/Show/0627ED05-BF19-4090-91FC-AD3865B40983-Some-name", new 
     { 
      controller = "Contacts", 
      action = "Show", 
      id = "0627ED05-BF19-4090-91FC-AD3865B40983", 
      name= "Some-name" 
     }); 
    } 
} 

Die Basis-Aufnahme:

public class RoutesTestClassBase<TAreaRegistration> 
{ 
    protected void TestRoute(string url, object expectations) 
    { 
     var routes = new RouteCollection(); 
     var areaRegistration = (AreaRegistration)Activator.CreateInstance(typeof(TAreaRegistration)); 

     // Get an AreaRegistrationContext for my class. Give it an empty RouteCollection 
     var areaRegistrationContext = new AreaRegistrationContext(areaRegistration.AreaName, routes); 
     areaRegistration.RegisterArea(areaRegistrationContext); 

     url = "~/" + areaRegistration.AreaName + "/" + url; 

     // Mock up an HttpContext object with my test path (using Moq) 
     var context = new Mock<HttpContextBase>(); 
     context.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath).Returns(url); 

     // Get the RouteData based on the HttpContext 
     var routeData = routes.GetRouteData(context.Object); 

     Assert.IsNotNull(routeData, "Should have found the route"); 
     Assert.AreEqual(areaRegistration.AreaName, routeData.DataTokens["area"]); 

     foreach (PropertyValue property in GetProperties(expectations)) 
     { 
      Assert.IsTrue(string.Equals(property.Value.ToString(), 
       routeData.Values[property.Name].ToString(), 
       StringComparison.OrdinalIgnoreCase) 
       , string.Format("Expected '{0}', not '{1}' for '{2}'.", 
       property.Value, routeData.Values[property.Name], property.Name)); 
     } 
    } 

    private static IEnumerable<PropertyValue> GetProperties(object o) 
    { 
     if (o != null) 
     { 
      PropertyDescriptorCollection props = TypeDescriptor.GetProperties(o); 
      foreach (PropertyDescriptor prop in props) 
      { 
       object val = prop.GetValue(o); 
       if (val != null) 
       { 
        yield return new PropertyValue { Name = prop.Name, Value = val }; 
       } 
      } 
     } 
    } 

    private sealed class PropertyValue 
    { 
     public string Name { get; set; } 
     public object Value { get; set; } 
    } 
} 
3

Um AreaRegistration.RegisterAllAreas zu Stellen() arbeiten, führen Sie den folgenden Code zuerst:

Bitte beachten Sie typeof(YourMvcSiteApplication).Assembly sollte Ihre MVC Web-Baugruppe führen !!!

object manager = typeof(BuildManager).GetField("_theBuildManager", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null); 
    manager.SetField("_skipTopLevelCompilationExceptions", true); 
    manager.SetField("_topLevelFilesCompiledStarted", true); 
    manager.SetField("_topLevelReferencedAssemblies", new List<Assembly> { typeof(YourMvcSiteApplication).Assembly }); 

Hier ist die Erweiterung Methode SetField() einer Instanz-Objekt:

public static void SetField<T>(this object source, string fieldName, T value) 
    { 
     var type = source.GetType(); 
     var info = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); 
     if (info != null) 
     { 
      info.SetValue(source, value); 
     } 
    } 

Die obigen Codes arbeitet für .NET 3.5, ich habe nicht testen für .NET 4 oder 4.5 noch!

3

Dies ist ein paar Jahre zu spät, aber ich dachte, ich würde teilen. Ich registriere alle Bereiche mit Reflektion.

public void RegisterAllAreas() 
{ 
    List<AreaRegistration> objects = new List<AreaRegistration>(); 

    foreach (Type type in Assembly.GetAssembly(typeof(MvcApplication)).GetTypes() 
      .Where(myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf(typeof(AreaRegistration)))) 
    { 
     objects.Add((AreaRegistration)Activator.CreateInstance(type)); 
    } 

    objects.ForEach(area => area.RegisterArea(new AreaRegistrationContext(area.AreaName, routes))); 
}