11

I am working on an ASP.NET MVC 4 web application that generates large and complicated reports. I want to write Unit Tests that render a View in order to make sure the View doesn't blow up depending on the Model:

 [Test]
 public void ExampleTest(){                  
     var reportModel = new ReportModel();

     var reportHtml = RenderRazorView(
           @"..\..\Report.Mvc\Views\Report\Index.cshtml", 
           reportModel);

     Assert.IsFalse(
         string.IsNullOrEmpty(reportHtml),
         "View Failed to Render!");          
 }

 public string RenderRazorView(string viewPath, object model){
    //WHAT GOES HERE?
 }

I have seen a lot of information about this around the web, but it's either arguing against testing vies, or can only be used in the context of a web request.

  • Arguing Against - Unit Testing the Views? - This concludes there should be no logic in the View so you should only need to test compilation. I think there is value in testing the View to make sure there aren't Null Reference Exceptions, the correct sections are shown, etc.
  • Context of a Web Request - Render a view as a string - This is to render a View to be sent in an email. But this approach requires being called via a web request (ie a valid HttpContextBase).

I have been working to adapt Render a view as a string to work with a Mocked HttpContextBase, but have been running into problems when using a Mocked ControllerContext:

Object reference not set to an instance of an object. at System.Web.WebPages.DisplayModeProvider.GetDisplayMode(HttpContextBase context) at System.Web.Mvc.ControllerContext.get_DisplayMode() at System.Web.Mvc.VirtualPathProviderViewEngine.GetPath(ControllerContext controllerContext, String[] locations, String[] areaLocations, String locationsPropertyName, String name, String controllerName, String cacheKeyPrefix, Boolean useCache, String[]& searchedLocations)

This is the code I have so far:

    public string RenderRazorView(string viewPath, object model)
    {
        var controller = GetMockedDummyController();

        //Exception here
        var viewResult = 
            ViewEngines.Engines.FindView(controller.ControllerContext, "Index", "");

        using (var sw = new StringWriter())
        {
            var viewContext =
                new ViewContext(
                    controller.ControllerContext,
                    viewResult.View,
                    new ViewDataDictionary(model),
                    new TempDataDictionary(),
                    sw);

            viewResult.View.Render(viewContext, sw);

            return sw.ToString();
        }
    }

I'm building the Controller:

    private Controller GetMockedDummyController()
    {
        var HttpContextBaseMock = new Mock<HttpContextBase>();
        var HttpRequestMock = new Mock<HttpRequestBase>();
        var HttpResponseMock = new Mock<HttpResponseBase>();
        HttpContextBaseMock.SetupGet(x => x.Request).Returns(HttpRequestMock.Object);
        HttpContextBaseMock.SetupGet(x => x.Response).Returns(HttpResponseMock.Object);

        var controller = new DummyController();

        var routeData = new RouteData();
        routeData.Values.Add("controller", "Dummy");

        controller.ControllerContext = 
            new ControllerContext(
                HttpContextBaseMock.Object,
                routeData,
                controller);

        controller.Url =
            new UrlHelper(
                new RequestContext(
                    HttpContextBaseMock.Object,
                    routeData), 
                new RouteCollection());

        return controller;
    }

The DummyController is just public class DummyController : Controller {}

Question

Give the path to a View, how can I render it to HTML from a Test project? Or more specifically, how can I mock out the ControllerContext.DisplayMode?

Community
  • 1
  • 1
Philip Pittle
  • 11,821
  • 8
  • 59
  • 123
  • I am having a similar issue. Did you eventually find any solution for that? I also wonder how do I mock out the ControllerContext.DisplayMode. – Shahin Apr 05 '16 at 11:41
  • 2
    Unfortunatly, I never found a good solution to do this and if I recall correctly, I had to abandon my effort as I blew through my Research Spike and didn't come back with anything useful. You might want to take a look at some of the new ASP.NET Core stuff as the testing story might be much better there. Part of what they've done, AFAIK is to isolate out `HttpContext`, which was one of the core problems here. – Philip Pittle Apr 05 '16 at 19:13

2 Answers2

5

Assuming you have complete separation of concerns, is it necessary to instantiate the controller at all? If not, then perhaps you can use RazorEngine to test your views.

var contents = File.ReadAllText("pathToView"); 
var result = Razor.Parse(contents,model);
// assert here
B2K
  • 2,541
  • 1
  • 22
  • 34
  • Nope, I don't need to invoke the Controller or it's methods. I already have a Model and want to use it to render a View, so I'll give this a try! – Philip Pittle Feb 10 '15 at 18:52
  • Have you used this to render "MVC" views. I can't get it to compile statements like `@Styles.Render()`. The engine complains that it's missing assemblies, even though I've added them. After looking at the compiled output, it looks like using statements are being added based on the web.config `system.web\pages\namespaces`. Is there a way to add these? – Philip Pittle Feb 16 '15 at 14:00
  • There is documentation in the RazorEngine link which discusses how to handle assemblies. Yes, I have used this for views, and string templates. I haven't used it for rendering nested templates though. – B2K Feb 16 '15 at 19:46
  • Here is a direct link to the RazorEngine documentation. http://antaris.github.io/RazorEngine/ – B2K Feb 17 '15 at 15:17
  • You can load your own assemblies with a custom reference resolver https://antaris.github.io/RazorEngine/ReferenceResolver.html – B2K May 25 '16 at 15:09
0

You will need an empty Controller just for testing (ex TestController)

public class WebMvcHelpers
{
    public static string GetViewPageHtml(object model, string viewName)
    {
        System.Web.Mvc.Controller controller = CreateController<TestController>();

        ViewEngineResult result = ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName);

        if (result.View == null)
            throw new Exception(string.Format("View Page {0} was not found", viewName));

        controller.ViewData.Model = model;
        StringBuilder sb = new StringBuilder();
        using (StringWriter sw = new StringWriter(sb))
        {
            using (System.Web.UI.HtmlTextWriter output = new System.Web.UI.HtmlTextWriter(sw))
            {
                ViewContext viewContext = new ViewContext(controller.ControllerContext, result.View, controller.ViewData, controller.TempData, output);
                result.View.Render(viewContext, output);
            }
        }

        return sb.ToString();
    }

    /// <summary>
    /// Creates an instance of an MVC controller from scratch 
    /// when no existing ControllerContext is present       
    /// </summary>
    /// <typeparam name="T">Type of the controller to create</typeparam>
    /// <returns></returns>
    public static T CreateController<T>(RouteData routeData = null)
                where T : System.Web.Mvc.Controller, new()
    {
        T controller = new T();

        // Create an MVC Controller Context
        HttpContextBase wrapper = null;
        if (HttpContext.Current != null)
            wrapper = new HttpContextWrapper(System.Web.HttpContext.Current);
        //else
        //    wrapper = CreateHttpContextBase(writer);


        if (routeData == null)
            routeData = new RouteData();

        if (!routeData.Values.ContainsKey("controller") && !routeData.Values.ContainsKey("Controller"))
            routeData.Values.Add("controller", controller.GetType().Name
                                                        .ToLower()
                                                        .Replace("controller", ""));

        controller.ControllerContext = new System.Web.Mvc.ControllerContext(wrapper, routeData, controller);
        return controller;
    }
}

public class TestController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}
Catalin
  • 11,503
  • 19
  • 74
  • 147
  • This doesn't seam to work. It still needs `HttpContext.Current`. Why is the `CreateHttpContextBase` method commented out? Do you have an implementation for that? – Philip Pittle Feb 16 '15 at 13:14
  • Hmm, no, i don't know where that method comes from. But [you can mock](http://stackoverflow.com/questions/4379450/mock-httpcontext-current-in-test-init-method) the `HttpContext.Current` instance – Catalin Feb 16 '15 at 13:25