views:

107

answers:

2

I am writing unit tests against my ASP.NET MVC application, in particular I am testing an HtmlHelper extension method that I wrote. There is a line inside of the extension method:

var innerHtml = htmlHelper.ActionLink(text, action, controller, routeValues, null);

When I run this inside of my unit test, the href of the generated URL is blank regardless of the action or controller passed in.

Here is my unit test:

var page = CreateProductDataPage(); //returns ProductDataPage object
var htmlHelper = Http.CreateHtmlHelperWithMocks<ProductDataPage>(new ViewDataDictionary<ProductDataPage>(page), false);
var result = htmlHelper.ProductListingBreadcrumb(true, null, null);

Here is the CreateHtmlHelperWithMocks method:

public static HtmlHelper<T> CreateHtmlHelperWithMocks<T>(ViewDataDictionary<T> viewData, bool isLoggedIn) where T : class
{
    var mockViewDataContainer = new Mock<IViewDataContainer>();
    mockViewDataContainer.SetupGet(v => v.ViewData).Returns(viewData);

    return new HtmlHelper<T>(GetViewContextMock(viewData, isLoggedIn).Object, mockViewDataContainer.Object);
}

Finally, here is the GetViewContextMock method:

public static Mock<ViewContext> GetViewContextMock(ViewDataDictionary viewData, bool isLoggedIn)
{
    var mock = new Mock<ViewContext>();

    mock.SetupGet(v => v.HttpContext).Returns(GetHttpContextMock(isLoggedIn).Object);
    mock.SetupGet(v => v.Controller).Returns(new Mock<ControllerBase>().Object);
    mock.SetupGet(v => v.View).Returns(new Mock<IView>().Object);
    mock.SetupGet(v => v.ViewData).Returns(viewData);
    mock.SetupGet(v => v.TempData).Returns(new TempDataDictionary());
    mock.SetupGet(v => v.RouteData).Returns(new RouteData());

    return mock;
}
A: 

I blogged about doing this with Rhino.Mocks about a month ago. You can find more information on how I handle this at http://farm-fresh-code.blogspot.com/2009/10/mocking-htmlhelper-class-with.html. Basically, my solution is to provide everything in the mock, both in the RouteData and via the ApplyAppPathModifier on the Response object connected to the mock helper. It's actually more of a fake helper based on underlying stubs.

tvanfosson
I think the point where you gave up in that post is exactly the point I am trying to overcome right now... I've been looking at the MVC source because they directly test ActionLink in there, but I can't figure out what piece I am missing.
mkedobbs
+2  A: 

Update: Figured it out. What a pain in the a$$. In case anyone else tries to do this...

The first step was to add the Route Collection from the global.asax in the creation of the mock HtmlHelper.

    public static HtmlHelper<T> CreateHtmlHelperWithMocks<T>(ViewDataDictionary<T> viewData, bool isLoggedIn) where T : class
    {
        var mockViewDataContainer = new Mock<IViewDataContainer>();
        mockViewDataContainer.SetupGet(v => v.ViewData).Returns(viewData);

        //These next two lines are key:
        var routeCollection = new RouteCollection();
        MvcApplication.RegisterRoutes(routeCollection);

        return new HtmlHelper<T>(GetViewContextMock(viewData, isLoggedIn).Object, mockViewDataContainer.Object, routeCollection);
    }

Then I had to make sure the HttpContext mock had a result being returned for the Request's ApplicationPath property and the Response's ApplyAppPathModifier method.

    public static Mock<HttpContextBase> GetHttpContextMock(bool isLoggedIn)
    {
        var context = new Mock<HttpContextBase>();
        var request = new Mock<HttpRequestBase>();
        var response = new Mock<HttpResponseBase>();
        var session = new Mock<HttpSessionStateBase>();
        var server = new Mock<HttpServerUtilityBase>();
        var principal = AuthenticationAndAuthorization.GetPrincipleMock(isLoggedIn);

        //These next two lines are required for the routing to generate valid URLs, apparently:
        request.SetupGet(r => r.ApplicationPath).Returns("/");
        response.Setup(r => r.ApplyAppPathModifier(It.IsAny<string>())).Returns((string r) => r);

        context.SetupGet(c => c.Request).Returns(request.Object);
        context.SetupGet(c => c.Response).Returns(response.Object);
        context.SetupGet(c => c.Session).Returns(session.Object);
        context.SetupGet(c => c.Server).Returns(server.Object);
        context.SetupGet(c => c.User).Returns(principal.Object);

        return context;
    }
mkedobbs
Thank you very much! You saved my day / evening / night. I agree, what a pain. Yes, they introduced interfaces and base classes open for mocking, but that doesn't mean they made it easy.
Thomas Eyde