Unit testing ASP.NET MVC 2 routes with areas bails out on AreaRegistration.RegisterAllAreas()
I'm unit testing my routes in ASP.NET MVC 2. I'm using MSTest and I'm using areas as well.
[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());
}
}
When it executes AreaRegistration.RegisterAllAreas()
however, it throws this exception:
System.InvalidOperationException: System.开发者_Go百科InvalidOperationException: This method cannot be called during the application's pre-start initialization stage.
So, I reckon I can't call it from my class initializer. But when can I call it? I obviously don't have an Application_Start
in my test.
I solved this by creating an instance of my AreaRegistration
class and calling the RegisterArea
method.
For example, given an Area named "Catalog" with this route:
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Catalog_default",
"Catalog/{controller}/{action}/{id}",
new {controller = "List", action = "Index", id = "" }
);
}
This is my test method:
[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"]);
}
I know I'm chiming in late here, but I just worked through this problem myself. Similar solution as Jason (register one area at a time), but like you I'm using MvcContrib.TestHelper instead of doing my own mocking.
[TestInitialize]
public void Setup() {
RouteTable.Routes.Clear();
var areaReg = new AdminAreaRegistration();
areaReg.RegisterArea(new AreaRegistrationContext(areaReg.AreaName, RouteTable.Routes));
}
[TestMethod]
public void admin_should_map_to_home() {
"~/Admin".ShouldMapTo<HomeController>(c => c.Index());
}
Note that MvcContrib has a hard dependency on Rhino Mocks. While I prefer using Moq, I'm fine with including the Rhino dll just to gain this nice functionality.
Well there is no place in test project you can put AreaRegistration.RegisterAllAreas(); to make it work, becouse it uses System.Web.Compilation.BuildManager class to compile code for website, and wich fails if it's called outside the ASP.NET pipeline. I think it's sort of bug, becouse it's realy makes tests very hard to run.
But I've invented a 2 step workaround :)
First you should modify App.Config file of your test project
<?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 you should refference all assemblies that contains AreaRegistration descenders. Second add this ugly code before 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);
This works only for .Net 4.0 and above
To Make AreaRegistration.RegisterAllAreas() working, run the following code first:
Please note typeof(YourMvcSiteApplication).Assembly
should be result in your MVC web assembly!!!
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 });
Here is the extension method SetField() of an instance object:
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);
}
}
The above codes works for .NET 3.5, I haven't test for .NET 4 or 4.5 yet!
This is a couple years late, but I figured I'd share. I'm registering all areas using reflection.
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)));
}
Here a nice version with combined approaches.
Code used from:
- Previous answer
- Phils approach
- Anonymous types as dictionaries
[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"
});
}
}
The base-fixture:
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; }
}
}
I think you are looking for the TestHelper class in the MVC Contrib library. Take a look at the tests in MVC Contrib (it is hidden away in there). You will find that everything is nicely mocked out.H
MVCContrib.UnitTests\TestHelper\RoutesTest.cs - must update the wiki! Good luck
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());
}
}
}
精彩评论