Unit Testing HtmlHelpers

I have a pretty decent project at my work that I am using ASP.Net MVC for.  It is a great framework and it’s my first real development project in a long time so I’m pretty excited.

I’m really stealing myself to use TDD, with very heavy emphasis on the first D, “Driven”.  I’m not allowing myself to write a line of code without a test first.  It’s a little hard because IntelliSense is pretty meaningless in tests if the actual objects haven’t even been built.  When things get a little muddy I go and create a quick empty controller or something before I finish the actual test.  This is just so that I can let VS 2008 and ReSharper work their magic adding namespaces and proper syntax.

Anyway, before I get into the nitty gritty I wrote this post because I was on a search of how to unit test my custom HtmlHelpers.  I found some decent posts and looked at the source for MvcContrib, but I didn’t need anything fancy.  I’m just creating a file input tag. I don’t need to mock the Context or anything.  I didn’t find a easy post anywhere on how to do this.  It turns out if you don’t need anything special it’s quite easy indeed.  If you’re looking for this then read on.

Anyway, I have a form where the user will upload a couple of files.  Using Scott Hanselman’s great blog post on unit testing uploading files using MVC I have a controller that now requires two non-empty files before it returns a success view.

I’ll flesh that out a little more with some actual validation of the upload contents but for now I’m refactoring my initial form.

From Scott’s post what I began with is the following:

This doesn’t make any use of HtmlHelpers so I wanted to refactor this.  There is no FileInput HtmlHelper but you could easily use the Textbox extension as in the following:

<%= Html.TextBox("tableA", null, new { enctype = "multipart/form-data" }) %>

And this would product the following correct HTML:


There are two things about this that bug me however.  First and foremost, did I save any effort?  The HtmlHelper code is actually longer then the straight HTML itself!  In my opinion HtmlHelpers should help in some way, not just be used because they are there.  So, if it doesn’t save me any extra work or protect me from something such as malformed urls or localization formatting, then there’s no point.  Second is that blank value attribute.  Why do I need that?

So, being the developer that I am, I thought I’d create my own HtmlHelper for a File Input textbox.  Using TDD this requires that I write my test first:

namespace Tests.CECWA.Web.Helpers{
  [TestFixture]
  public class FileInputExtensionsTests
  {
    [Test]
    public void CanGetCorrectOutput()
    {
      string html = FileInputExtensions.FileInput(null, "testName");
      const string expected = "";
      Assert.That(html, Is.EqualTo(expected));
    } 
  } 
}

I use NUnit but it’s pretty similar in any testing framework.  FileInputExtensions is my extension class containing my custom HtmlHelper FileInput.  The null parameter is the HtmlHelper class that is actually extended.  Since I’m not making use of the Context or anything else I can simply pass a null in.  If I were doing something fancy such as needing the base path of the app then I’d have to mock this out like the other more extensive posts do.

This test simply calls my extension method and stores the result in the html variable.  I store what I expect in my expected variable.  Then I assert that they are equal.  Quite easy actually.

When I build my project it of course doesn’t compile since there is no FileInputExtensions class.  So I do the bare minimum to compile:

namespace CECWA.Web.Helpers
{    
  public static class FileInputExtensions    
  {        
    public static string FileInput(this HtmlHelper helper, string name)        
    {            
      throw new NotImplementedException();        
    }    
  }
}

Everything now compiles but my test fails.  So, lets complete the method:

public static string FileInput(this HtmlHelper helper, string name)
{
  return String.Format("", name); 
} 

All set.  My test passes and now I’ve got an extension method all ready to use.

After creating another test and extension method for a Label element I have now refactored my View above to the following:

<% Html.BeginForm("ReceiveCASEMISTables", "SELPA", FormMethod.Post, new {enctype = "multipart/form-data"}); %>
  <%= Html.Label("tableA", "Table A Filename:") %>
  <%= Html.FileInput("tableA") %> 
<%= Html.Label("tableB", "Table B Filename:") %> <%= Html.FileInput("tableB") %>
<%= Html.FormHelper().Submit("Upload Files") %> <% Html.EndForm(); %>

OK, if you compare the pure HTML version and this one you don’t see much difference in the “effort”.  However, this is more protected against improper paths in my form action url, typos in my various field ID’s, and just general lousy HTML coding in general.

Multiply this over several dozen pages and you start to feel a real tight management on your html output and maintainability.  For instance, if I want to change the way my FileInput extension renders I simply update my test, update my class to pass and now every place a FileInput helper is used has been updated through out the site.  Nice!

Technorati Tags: ,

3 Replies to “Unit Testing HtmlHelpers”

  1. Your example of testing HtmlHelper extension methods, unfortunately, does not apply to methods that will actually use properties and methods of the HtmlHelper class. In your case, you are simply calling your method statically, passing in a null HtmlHelper reference. Your case is very simple, but in many cases, where you would be using the HtmlHelper object, you would need to provide quite a good deal of mocking/stubbing in order for that to work properly.

  2. Justin,
    Thanks for the comment. You are completely correct, however, since I didn’t need to make use of any of the HtmlHelper members I didn’t bother to create a fake object for the class.

    If you do need to make use of the members then you should be able to use one of the popular fake object frameworks to create a stub or mock of the HtmlHelper class.

Comments are closed.