Thursday, April 14, 2011

How to mock the MVC View Engine

Let's say we've created an HtmlHelper extension method to render a partial view without a need to provide a partial view name based on the type or template hint of a view model. The helper would look like:

public static void DisplayModel(this HtmlHelper htmlHelper, object model)
{
    var type = model.GetType();
    var modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, type);
    if (string.IsNullOrEmpty(modelMetadata.TemplateHint))
    {
        htmlHelper.RenderPartial("DisplayTemplates/" + modelMetadata.ModelType.Name, model);
    }
    else
    {
        htmlHelper.RenderPartial("DisplayTemplates/" + modelMetadata.TemplateHint, model);
    }
}
This method is quite simple. It employs the built-in extension method of HtmlHelper: RenderPartial(). If you read the MVC source code, you will find that this method requires the ViewEngines to find the provided viewName and return a viewEngineResult. Then the viewEngineResult will be used to render the View content to Html. So it's quite complicated to test something that requires a ViewEngine behind the scene. I fell into this sittuation when i tried to test my extension methods (I attempt to raise the coverage for our team unit tests, started by writing unit tests for all the shits I made before.). After digging into the MVC source code, I think I need to replace the default view engines in ViewEngine.Engines collection by a mocked ViewEngine. Here is the implementation:
public static ViewEngineResult SetupViewContent(string viewName, string viewHtmlContent)
{
    var mockedViewEngine = Substitute.For<IViewEngine>();
    var resultView = Substitute.For<IView>();
    resultView.When(x => x.Render(Arg.Any<ViewContext>(), Arg.Any<TextWriter>())).Do(c =>
    {
        var textWriter = c.Arg<TextWriter>();
        textWriter.Write(viewHtmlContent);
    });
    var viewEngineResult = new ViewEngineResult(resultView, mockedViewEngine);

    mockedViewEngine.FindPartialView(Arg.Any<ControllerContext>(), viewName,  Arg.Any<bool>()).Returns(viewEngineResult);
    mockedViewEngine.FindView(Arg.Any<ControllerContext>(), viewName, Arg.Any<string>(), Arg.Any<bool>()).Returns(viewEngineResult);

    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(mockedViewEngine);
    return viewEngineResult;
}
As i said in my previous posts, I'm using NSubstitute as my primary mock framework due to it's simplicity and easy to use. To setup all the things for my test, I'll create a mock for the IView which will be used to initialize the ViewEngineResult. And the ViewEngineResult will be the output for our mocked IViewEngine when it's method FindView or FindPartialView is executed. The most interesting point to do these stuff is that the default viewEngine collection in MVC3 has 2 item which are Aspx view engine and razor view engine. Therefore, to make our mocked view engine to be able to work, we need to clear these 2 default view engines and insert our mocked view engine. That's it. Here is one of the tests:
[TestMethod]
public void Can_use_a_model_type_as_partial_view_name_and_display_model_with_provided_view_model()
{
    // Arrange
    var builder = MvcTestControllerBuilder.GetDefaultBuilder();
    var htmlHelper = builder.GetHtmlHelper();
    htmlHelper.ViewContext.Writer = Substitute.For<StringWriter>();
    MvcTestFixtureHelper.SetupViewContent("DisplayTemplates/ProfileViewModel", "ProfileViewModel Content");

    // Action
    htmlHelper.DisplayModel(new ProfileViewModel());

    // Assert
    htmlHelper.ViewContext.Writer.Received().Write("ProfileViewModel Content");
}
Please refer to this post about MvcTestControllerBuilder. Cheers.

1 comments:

James Barker said...

Exactly what I needed. I did however use Moq, so Setup(...).Callback(...) to equal When(...).Do(...).

Functionality I was testing:
- ViewEngines.Engines.FindView
- ViewEngines.Engines.FindPartialView

Post a Comment