Saturday, June 25, 2011

Unit test your MVC views using Razor Generator

Click here to find all the posts relating to the Razor Generator

A few days ago, I blogged about how you can use Razor Generator to precompile your MVC Razor views.

In this post, I will demonstrate how you can then unit test your precompiled views. Note that this is still very much experimental, so at this point the primary goal is to get feedback on the concept.
 

Simple walkthrough to unit test views

After installing RazorGenerator, create an MVC 3 Razor app, using the ‘Internet Application’ template and including the unit test project.

In the previous post, we used precompiled views in a different library, so this time let’s keep them in the MVC project to show something different.

First, use NuGet to install the RazorGenerator.Mvc package in your MVC project.
 
Then, as in the previous post, set the custom tool on Views\Home\Index.cshtml to ‘RazorGenerator’, causing Index.cs to be generated under it.
 
But now, let’s do something new and use NuGet again to add the RazorGenerator.Testing package to the unit test project (not to the MVC app!).
 
And that’s all it takes to get set up! Now we can write a unit test for our precompiled Index.cshtml view. e.g. create a Views\HomeViewsTest.cs (in the unit test project):
 
using HtmlAgilityPack;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcApplication2.Views.Home;
using RazorGenerator.Testing;

namespace MvcApplication1.Tests.Views {
    [TestClass]
    public class HomeViewsTest {
        [TestMethod]
        public void Index() {
            // Instantiate the view directly. This is made possible by
            // the fact that we precompiled it
            var view = new Index();

            // Set up the data that needs to be accessed by the view
            view.ViewBag.Message = "Testing";

            // Render it in an HtmlAgilityPack HtmlDocument. Note that
            // you can pass a 'model' object here if your view needs one.
            // Generally, what you do here is similar to how a controller
            //action sets up data for its view.
            HtmlDocument doc = view.RenderAsHtml();

            // Use the HtmlAgilityPack object model to verify the view.
            // Here, we simply check that the first <h2> tag contains
            // what we put in view.ViewBag.Message
            HtmlNode node = doc.DocumentNode.Element("h2");
            Assert.AreEqual("Testing", node.InnerHtml.Trim());
        }
    }
}

A few notes about unit testing views

Unit testing views in ASP.NET MVC is something that was very tricky to do before, due to the fact that the views are normally compiled at runtime. But the use of the Razor Generator makes it possible to directly instantiate view classes and unit test them.

Now the big question, is whether unit testing views is desirable. Some people have expressed concerns that it would be a bit fragile due to the changing nature of the HTML output.
 
My take here is that while it would be a bad idea to try to compare the entire HTML output, the test can be made pretty solid by selectively comparing some interesting fragments, as in the sample above.
 
That being said, I have not tried this is a real app, so there is still much to learn about how this will all play out. This is just a first step!
 

What about partial views?

When designing this view testing framework, we took the approach that we wanted to focus on the output of just one view at a time. Hence, if a view calls @Html.Partial(…) to render a sub-view, we don’t let the sub-view render itself, and instead just render a token to mark where the sub-view would be.

This seemed more true to the nature of what a unit test should be, compared to letting the whole composite page render itself, which would be more of a functional test (plus there were some tough challenged to making it work).
 

Where do we go from here?

Well, it’ll be interesting to hear what people think about the general idea. We’re interested in two types of feedback.

First, what do you think about the overall concept of unit testing views using this approach. Second, please report bugs that you run into to http://razorgenerator.codeplex.com/. At this point, I expect it to be a bit buggy and probably blow up on some complex views. Treat it as a proof of concept! :)

41 comments:

  1. I like concept. I would like to test partials and views without the master page, if that is even possible. This has been an area that we have been struggling to get under control.

    ReplyDelete
  2. @Eric: that's actually the way it works. In the example above, the master is not rendered.

    ReplyDelete
  3. I wait to try this! I have been trying hard to find a way to test my views for runtime exceptions, or even compile time exceptions without having my recompile process take ages (with MvcBuildViews)

    ReplyDelete
  4. @KallDrexx: indeed, the ability to get compile errors early is another benefit that I forgot to point out :)

    ReplyDelete
  5. Interesting!

    Thanks for the work done. As soon as I saw this I was thinking... wow I can use this to speed up some UI tests I have. But while trying to use this I just realize that the thing I'm testing in my UI are the interactions, more than the representation.

    I don't see how my UI tests could use this type of checks. With this, most of the tests I'll end up writing will be how things are presented (rendered) but that's not how I'm testing my UI. If I follow the HOW versus the WHAT I'll end up with "fragile tests" for my UI.

    Probably this can be used for validate that the view display the "error summary" when server side errors?

    I'll keep thinking (and looking) for places where I can use it, because definitely this is going to be fast!!

    One advantage that I see is that it can detect errors on my razor views just in time. I'm having some issues with this, if for example I rename a variable (using Resharper) all usages in razor views are not changed, and I just realize of this when running that view, too bad. I can unit test that, but is silly, just building should be enough, after all, we have strongly type check :)

    Thanks one again for this work, I'll try to find places where this could bring real value on my tests and give you the feedback.

    --Erlis

    ReplyDelete
  6. @Erlis: Thanks for your feedback. Agreed that this is not a replacement for full browser-based UI tests. This is really just to test once little piece of UI at a time outside of the context of the full page, and without involving interactions.

    Whether this will turn out to have practical useful uses is yet to be seen. Still very much an experiment at this point! :)

    ReplyDelete
  7. I can't seem to get this to work. My unit test is:

    [TestMethod]
    public void Testing()
    {
    var view = new Index();
    view.ViewBag.Message = "Testing";
    view.Execute();
    }

    The Execute() call fails with a NullReferenceException referencing my cshtml file at the ViewBag line:

    @{
    ViewBag.Title = "Home Page";
    }

    Debugging shows that ViewBag isn't null, ViewBag.Title is set correctly to "Testing" at the exception, and I'm not sure how to proceed from there. The @{ is the first line of my view.

    ReplyDelete
  8. This comment has been removed by the author.

    ReplyDelete
  9. @KallDrexx: Not sure; I'd need to look at the view. Or was that with a new MVC app as in my post? If needed, you can file a bug at http://razorgenerator.codeplex.com/, which will make it easier to discuss. Thanks!

    ReplyDelete
  10. This is with an existing MVC3 app that I currently have been developing. In your example, where do you get the RenderAsHtml() method off your view? I don't have that.

    ReplyDelete
  11. You need to add a 'using PrecompiledMvcViews.Testing' in your file (as in my sample). Make sure you install the 'PrecompiledMvcViews.Testing' package in the test project first.

    ReplyDelete
  12. Doh, that's what I get for skimming :). Everything's working now. I guess Execute() is for the Asp.net infrastructure, but RenderAsHtml() works as it should!

    I have already used it in one test scenario to detect if a view's loop code is incorrect and the index goes out of bounds and in another real test case to verify that my controller is passing the same model class that the view is expecting, so I don't get errors where the view's model and what the controller passes to the view differ.

    This is excellent and just what I was looking for :) Thanks a lot!

    ReplyDelete
  13. @KallDrexx: good to hear, thanks for the feedback!

    ReplyDelete
  14. Sorry for the barrage of (probably stupid) questions but I have noticed one issue that I don't know if it's my mistake or not. It seems that any HtmlHelpers I have referenced in my web.config razor namespaces configuration are no longer effective once I modify my view to use the RazorGenerator.

    The main issue right now is my Telerik calls are giving compile time errors. They work perfectly fine when not using the precompiled view, but once I precompile them I get "'System.Web.Mvc.HtmlHelper' does not contain a definition for 'Telerik'"

    Is something required for the precompiled views to use the non-standard html helpers?

    ReplyDelete
  15. That's a limitation in the current build that we'd like to address. For now, you can work around it by adding a using in the Razor view. e.g.
    @using System.Web.Mvc.Ajax

    I realize it could be painful if you have many views that need that, but hopefully it's just temporary till we fix it.

    If you don't mind, try to use the Discussions and Issue Tracker in http://razorgenerator.codeplex.com/ as it makes it easier to have conversations focused on one topic :)

    ReplyDelete
  16. It would be nice if we could use CSS selectors to get the HTML elements.

    ReplyDelete
  17. @Acaz: note that HtmlAgilityPack is an unrelated project that this is using as an object model. Seems they have a bug tracking that, so vote it up: http://htmlagilitypack.codeplex.com/workitem/28970

    ReplyDelete
  18. Hi David,

    Thanks for two great blog posts! The concept looks interesting, It would be even much better if there was an easy way of setting the custom tool for building views to RazorGenerator on a project-wide level. It's a bit tedious having to do this with every new view by hand.

    ReplyDelete
  19. @Adrian: agreed, this is a pain point today. Would you mind opening a bug on http://razorgenerator.codeplex.com/ to track this? Thanks!

    ReplyDelete
  20. This is excellent - I'm thinking of creating a utility method using this which will allow us to specify a form, with name, and NameValueCollection as fields, and I could effectively unit test the fact that the view correctly renders a form which would (should?) post back correctly...

    Whilst I appreciate that in the end we cannot avoid having full web integration tests, it seems no bad thing if we can test as many things in isolation as possible...

    ReplyDelete
  21. @william: thanks for the positive feedback! :)

    ReplyDelete
  22. Hi David, How can I do for RazorGenerator generates the precompiled views files in a different project of my Views (.cshtml) files? I mean, specify a custom destination path instead a nested file

    ReplyDelete
  23. @Rarvick: that seems unrelated to the Unit Testing aspect covered by this post. Could you start a discussion on http://razorgenerator.codeplex.com/ to discuss your issue? Thanks!

    ReplyDelete
  24. Thanks David, I've started a discussion about, the title is "Custom path of precompiled files". I would appreciate your help.
    The link is : http://razorgenerator.codeplex.com/workitem/31

    ReplyDelete
  25. Hi David. How would I go about implementing a button click? Thanks. Mike

    ReplyDelete
  26. @MikeP: this is not meant for full UI interaction testing. For that, alternate testing techniques are better (e.g. automate the browser, or similar).

    ReplyDelete
  27. How do I unit test views that use a master page. When I execute RenderAsHtml() the view stops processing after the @Layout = "~/Views/Shared/_Layout.cshtml" at the top of the view.

    ReplyDelete
  28. @Dan: please start a thread on http://razorgenerator.codeplex.com/, with more details about what you're trying and seeing. It'll be easier to discuss there. Thanks!

    ReplyDelete
  29. Hi david. Thanks for this tutorial. I need to know though, in mvc, which line actually does the job of making the .cshtml into c# source? I have stepped through the code and I was wondering if it is this:
    Type type = BuildManager.GetCompiledType(ViewPath);
    (from BuildManagerCompiledView.cs)

    ReplyDelete
  30. @Elliot: yes GetCompiledType will cause the source to be generated and compiled on the fly if it has not already been done for that file (then it is cached). Note that this is not directly related to this topic, so please follow up on the MVC forum or StackOverflow (and you can point me to the thread if you like).

    ReplyDelete
  31. This comment has been removed by the author.

    ReplyDelete
  32. @Robert: please use http://razorgenerator.codeplex.com/ to discuss any RazorGenerator related topics. Thanks!

    ReplyDelete
  33. Wonderful, thanks for making this. I came here while searching for ideas on testing Razor views after leaving out a hidden field for the model's item ID and wondering if there was a sensible way to test for this sort of error.

    But the immediate benefit to me of your package is the same as KallDrexx's - compile-time errors for Razor views without setting MvcBuildViews (which seemed to cause havoc for the rest of my team).
    Perfect!

    ReplyDelete
  34. Sounds like an interesting concept! As for the partial views and the layout, I would most naturally consider them as dependencies. The compiled view should allow to inject them, so in production the compiled objects from the partial views could be injected into the view that uses them. For unit testing in turn they could be mocked (e.g. rendering a marker as you mentioned would be done by your current implementation).

    ReplyDelete
  35. It is a great idea! Especially since razor pages can easily be built with separation of concerns! This allows for great modular and testable web applications! Thanks for the work you have done!

    ReplyDelete
  36. I can't get this to work, it fails on RenderAsHtml(); with An HttpContext is require to perform this action.

    [Then(@"the view should have a username input field")]
    public void ThenTheViewShouldHaveAUsernameInputField()
    {
    var view = new MvcSpecFlow.Views.Account.Register();
    var doc = view.RenderAsHtml();
    var username = doc.DocumentNode.Element("Username");
    Assert.IsNotNull(username);
    //ScenarioContext.Current.Pending();

    }

    ReplyDelete
    Replies
    1. apparently it was this line in the view: @Html.AntiForgeryToken()

      Delete
  37. @Brandon, yes, this is still pretty experimental. But please use http://razorgenerator.codeplex.com/ for all RazorGenerator discussions.

    ReplyDelete
  38. #David,nice piece of work.How will i go about in testing a view in MVC 4,or is it the same?i am doing an internet app. how would i go about in testing this view




    var db = Database.Open("DefaultConnection");
    var dbdata = db.Query("SELECT Course_Description, Amnt_Made FROM Most_Booked_Course");
    var myChart = new Chart(width: 600, height: 400)
    .AddTitle("COURSE STATISTICS")
    //.AddSeries(chartType: "Pie")
    .DataBindTable(dataSource: dbdata, xField: "Course_Description")
    .Write();
    }

    ReplyDelete
  39. @manqoba: But please use http://razorgenerator.codeplex.com/ for all RazorGenerator discussions.

    ReplyDelete