15

Trying to do some controller unit-testing in my ASP.NET MVC 3 web application.

My test goes like this:

[TestMethod]
public void Ensure_CreateReviewHttpPostAction_RedirectsAppropriately()
{
   // Arrange.
   var newReview = CreateMockReview();

   // Act.
   var result = _controller.Create(newReview) as RedirectResult;

   // Assert.
   Assert.IsNotNull(result, "RedirectResult was not returned");
}

Pretty simple. Basically testing a [HttpPost] action to ensure it returns a RedirectResult (PRG pattern). I'm not using RedirectToRouteResult because none of the overloads support anchor links. Moving on.

Now, i'm using Moq to mock the Http Context, including server variables, controller context, session, etc. All going well so far.

Until i've hit this line in my action method:

return Redirect(Url.LandingPageWithAnchor(someObject.Uri, review.Uri);

LandingPageWithAnchor is a custom HTML helper:

public static string LandingPageWithAnchor(this UrlHelper helper, string uri1, string uri2)
{
   const string urlFormat = "{0}#{1}";

   return string.Format(urlFormat,
                helper.RouteUrl("Landing_Page", new { uri = uri1}),
                uri2);
}

Basically, i redirect to another page which is a "landing page" for new content, with an anchor on the new review. Cool.

Now, this method was failing before because UrlHelper was null.

So i did this in my mocking:

controller.Url = new UrlHelper(fakeRequestContext);

Which got it further, but now it's failing because the route tables don't contain a definition for "Landing_Page".

So i know i need to mock "something", but im not sure if it's:

a) The route tables
b) The UrlHelper.RouteUrl method
c) The UrlHelper.LandingPageWithAnchor extension method i wrote

Can anyone provide some guidance?

EDIT

This particular route is in an Area, so i tried calling the area registration in my unit test:

AreaRegistration.RegisterAllAreas();

But i get an InvalidOperationException:

This method cannot be called during the application's pre-start initialization stage.

RPM1984
  • 72,246
  • 58
  • 225
  • 350
  • I found the UrlHelper hard to mock, at least with Rhino Mocks. I have a solution, but it's not particularly elegant, basically faking rather than mocking. I'm interested to see what answers you get. If you don't get anything, let me know and I'll post some code tomorrow -- I don't have access to it right now. Using an extension method complicates things since you need to have an instance of the helper to work from and you can't inject one into your extension. – tvanfosson May 10 '11 at 00:06
  • Have you looked at this question? It has lots of meat on it: http://stackoverflow.com/questions/674458/asp-net-mvc-unit-testing-controllers-that-use-urlhelper – Matt Greer May 10 '11 at 00:17
  • @Matt Greer - looks useful. But again, i'm using "areas" - and i'm getting an error when trying to call that method from my unit test. I should have put that in the question - editing now... – RPM1984 May 10 '11 at 00:38

3 Answers3

12

Got it working by mocking the HttpContext, RequestContext and ControllerContext, registering the routes then creating a UrlHelper with those routes.

Goes a little like this:

public static void SetFakeControllerContext(this Controller controller, HttpContextBase httpContextBase)
{
    var httpContext = httpContextBase ?? FakeHttpContext().Object;
    var requestContext = new RequestContext(httpContext, new RouteData());
    var controllerContext = new ControllerContext(requestContext, controller);
    MvcApplication.RegisterRoutes();
    controller.ControllerContext = controllerContext;
    controller.Url = new UrlHelper(requestContext, RouteTable.Routes);
}

FakeHttpContext() is a Moq helper which creates all the mock stuff, server variables, session, etc.

RPM1984
  • 72,246
  • 58
  • 225
  • 350
  • 2
    I also used the Moq helpers provided by Scott Hanselman [here](http://www.hanselman.com/blog/ASPNETMVCSessionAtMix08TDDAndMvcMockHelpers.aspx). – Dan Atkinson Aug 25 '11 at 21:44
  • Since the RTM, UrlHelper now requires an HttpRequestMessage in the constructor, so we're at square one all over again. Could you update your response to reflect this change? – Ismael Aug 29 '12 at 11:04
  • @cadessi - if your referring to MVC 4, i haven't upgraded yet - but i will in the next couple of weeks. Once i do, i'll update. :) – RPM1984 Aug 29 '12 at 23:42
  • @cadessi, we've used MVC 4 and 5 with no need for an HttpRequestMessage in the UrlHelper constructor. – ps2goat Apr 28 '14 at 22:25
0

There is a constructor for UrlHelper that takes a RouteCollection as a second argument. If you have the default setup that MVC creates for you, then I think this should work for you:

var routes = new RouteCollection();
MvcApplication.RegisterRoutes(routes);

controller.Url = new UrlHelper(fakeRequestContext, routes);

An alternative is to modify how your application starts up to make things a little easier to test and work with. If you define and interface like this:

public interface IMvcApplication
{
    void RegisterRoutes(RouteCollection routes);
    // Other startup operations    
}

With an implementation:

public class MyCustomApplication : IMvcApplication
{
    public void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        // Your route registrations here
    }

    // Other startup operations
}

Then you can modify your Global.asax like this:

public class MvcApplication : HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        var app = new MyCustomApplication();
        app.RegisterRoutes(RouteTable.Routes);
        // Other startup calls
    }
}

And still have the flexibility to register your routes for testing. Something like this:

private IMvcApplication _app;
private RouteCollection _routes;

[TestInitialize]
public void InitializeTests()
{
    _app = new MyCustomApplication();

    _routes = new RouteCollection();
    _app.RegisterRoutes(_routes);
}

This can be similarly leveraged to work with area registrations as well.

private RouteCollection _routes;
private MyCustomAreaRegistration _area;

[TestInitialize]
public void InitTests()
{
    _routes = new RouteCollection();
    var context = new AreaRegistrationContext("MyCustomArea", _routes);

    _area.RegisterArea(context);
}
ataddeini
  • 4,931
  • 26
  • 34
  • I saw that, but the routes are created in global.asax, as well as in the area registrations. I don't have access to MvcApplication.RegisterRoutes(routes); Do i have to "recreate" fake routing entries? – RPM1984 May 10 '11 at 00:19
  • @RPM1984: You can't call `MvcApplication.RegisterRoutes(routes);` from your tests? I would think you'd be able to, but if you can't you may consider implementing an interface to define your application startup methods and call it that way. I can provide some more example code if you'd like. – ataddeini May 10 '11 at 00:22
  • this particular route is in an area. So i tried calling `AreaRegistration.RegisterAllAreas();` right before `MvcApplication.RegisterRoutes(routes);` - but i get an error saying "this method cannot be called during the applications pre-start initialization stage." – RPM1984 May 10 '11 at 00:34
  • @RPM1984: Ah, ok. I actually haven't worked with areas that much but I'll try to expand my answer with some information that's useful. – ataddeini May 10 '11 at 00:36
0

Because this is a test, your test suite is most likely not reading the Global.asax like you may be expecting.

To get around this, do what @ataddeini suggested and create a route collection. To this collection, add in a new route, which will look like...

var routes = new RouteCollection();
routes.Add(new Route("Landing_Page", new { /*pass route params*/}, null));
var helper = new UrlHelper(fakeRequestContext, routes);
Community
  • 1
  • 1
DavidAndroidDev
  • 2,371
  • 3
  • 25
  • 41