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.
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.
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.
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.
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.
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@Eric: that's actually the way it works. In the example above, the master is not rendered.
ReplyDeleteI 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@KallDrexx: indeed, the ability to get compile errors early is another benefit that I forgot to point out :)
ReplyDeleteInteresting!
ReplyDeleteThanks 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
@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.
ReplyDeleteWhether this will turn out to have practical useful uses is yet to be seen. Still very much an experiment at this point! :)
I can't seem to get this to work. My unit test is:
ReplyDelete[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.
This comment has been removed by the author.
ReplyDelete@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!
ReplyDeleteThis 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.
ReplyDeleteYou 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.
ReplyDeleteDoh, 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!
ReplyDeleteI 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!
@KallDrexx: good to hear, thanks for the feedback!
ReplyDeleteSorry 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.
ReplyDeleteThe 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?
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.
ReplyDelete@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 :)
Will do :)
ReplyDeleteIt would be nice if we could use CSS selectors to get the HTML elements.
ReplyDelete@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
ReplyDeleteHi David,
ReplyDeleteThanks 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.
@Adrian: agreed, this is a pain point today. Would you mind opening a bug on http://razorgenerator.codeplex.com/ to track this? Thanks!
ReplyDeleteThis 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...
ReplyDeleteWhilst 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...
@william: thanks for the positive feedback! :)
ReplyDeleteHi 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@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!
ReplyDeleteThanks David, I've started a discussion about, the title is "Custom path of precompiled files". I would appreciate your help.
ReplyDeleteThe link is : http://razorgenerator.codeplex.com/workitem/31
Hi David. How would I go about implementing a button click? Thanks. Mike
ReplyDelete@MikeP: this is not meant for full UI interaction testing. For that, alternate testing techniques are better (e.g. automate the browser, or similar).
ReplyDeleteHow 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@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!
ReplyDeleteHi 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:
ReplyDeleteType type = BuildManager.GetCompiledType(ViewPath);
(from BuildManagerCompiledView.cs)
@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).
ReplyDeleteThis comment has been removed by the author.
ReplyDelete@Robert: please use http://razorgenerator.codeplex.com/ to discuss any RazorGenerator related topics. Thanks!
ReplyDeleteWonderful, 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.
ReplyDeleteBut 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!
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).
ReplyDeleteIt 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!
ReplyDeleteI can't get this to work, it fails on RenderAsHtml(); with An HttpContext is require to perform this action.
ReplyDelete[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();
}
apparently it was this line in the view: @Html.AntiForgeryToken()
Delete@Brandon, yes, this is still pretty experimental. But please use http://razorgenerator.codeplex.com/ for all RazorGenerator discussions.
ReplyDelete#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
ReplyDeletevar 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();
}
@manqoba: But please use http://razorgenerator.codeplex.com/ for all RazorGenerator discussions.
ReplyDelete