views:

767

answers:

5

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.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.

+1  A: 

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

er-v
Thanks. Although it's not very pretty, it does make my test pass. I found it to fail when debugging the test though, throwing a different exception (something about a filename containing an illegal character). Anyway, I'll accept your answer for now. I'm still hoping for a proper solution.
Sandor Drieënhuizen
+1  A: 

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());
        }

    }
}
todd
As you can see in my code example, I'm already using the MvcContrib test helper (the `ShouldMapTo` extension method call). There doesn't seem to be specific support for areas in MvcContrib at the moment or they haven't made it evident. Even if there is sufficient support for testing areas, I'd still have the problem that my route registration code throws this exception when called from a unit test due to the `AreaRegistration.RegisterAllAreas()` call.
Sandor Drieënhuizen
+2  A: 

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"]);
}
Jason Capriotti
I guess that's a good solution, thanks.
Sandor Drieënhuizen
+3  A: 

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.

Todd Menier
A: 

Here a nice version with combined approaches.

Code used from:


[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; }
    }
}
Lars Corneliussen