Coding, Testing

ApprovalTests and MVC Views: Working With Data

In part one of ApprovalTests and MVC Views, Getting Started, I covered the basic mechanics of getting an MVC View under test, but the pages I tested weren’t very interesting.  Those pages were dynamic only in the slimmest sense (they displayed the user identity).  In this article we will introduce some data models into our testing scenario so that we can see what testing more realistic pages look like.

Just a reminder, we started this example at File->New Project and our goal is to create a set of tests that run consistently under Visual Studio, NCrunch, and CC.NET.  All of the source code is available as a mercurial repository on bitbucket.org.  Finally, if you are interested in testing MVC Views, a great place to get started (other than this blog series) is by watching Llewellyn Falco’s video on the subject: Using ApprovalTests in .Net 20 Asp.Mvc Views.

Introducing Models

Designing and testing Models are not that different than testing any other .NET class, and smarter people than me have already covered the subject. I won’t go into details about the models for this example site, but if you are interested you can see them in the code repository that accompanies the article. The only feature of interest in this example’s model is the repository implementation. Our repository is never available, any attempt to read data from the default repository implementation will throw a NotImplementedException. This behavior is meant to simulate an unavailable SQL server, but NotImplementedExceptions are easier to generate than SQLExceptions.

After creating the models, I deleted the default Index view and replaced it with a strongly typed list view using the default tools in Visual Studio. The test immediately fails in NCrunch with a NullReferenceException because current controller implementation does not pass a Model to the new View. We have broken our Index View test, and also broken one of the original tests provided by Visual Studio.

Lets replace the broken Controller test with something that works, then move on to our View test. In the interest of readability, this test has been formatted to fit your screen:

[TestMethod]
public void Index()
{
  // Arrange
  Mock<ICarRepository> repositoryMock 
  repositoryMock = new Mock<ICarRepository>();
  repositoryMock.DefaultValue = DefaultValue.Mock;
  HomeController controller;
  controller = new HomeController(repositoryMock.Object);

  // Act
  controller.Index();

  // Assert
  repositoryMock.Verify(repo => repo.GetCars());
}

Since this is a Controller test we have the option to create an instance and manipulate its state. So I create a mock repository and pass it to the HomeController, call Index, and use the mock to verify that Index method is trying to get its data from the repository. Whether or not that data access call should be locked down by a test, or simply kept hidden as an implementation detail is up to you. The point of showing this example is to remind you that this is exactly the kind of thing you can’t do with a View test. Any changes to the Controller instance created in the test method will not be reflected in the View when it’s compiled on the web server and delivered to MvcApprovals.

In this example, I do want the data access to remain locked down so that this test will ensure me that I don’t break the default behavior as I create seams to get my View test to work. In “production” we want the call to Index to hit the repository and bring down the cars. Naturally, I had to make some changes to the HomeController after writing this test, and after overloading the constructor, adding a field for the repository, and putting the necessary method call into the Index method, the test passes, but our View test still fails.

We know that we have an uncooperative repository that always throws exceptions, and that might be a reason its failing. Sure enough, it is.

mvcviewtest4_thumb2

We need to resolve that issue before we can approve the new version of the index page. We can’t resolve the issue by creating Mocks in our test method. Remember that the controller instance we create in the test is just a convenient way to pass metadata to MvcApprovals. The real code that we’re testing is the code on the web server. The web server has its own instance of the HomeController, which it instantiated using the default constructor.  In our case, the default constructor happens to create an instance of the unusable repository. The key to testing this new Index view is to create a seam that lets us avoid the repository entirely.

Once we create seams, we can add MVC actions that pass fake data into the seams for us. These new actions have to be inside the production site, not the test project.  The web server needs to see and execute the test actions for us.  After we create the test actions, we update our View tests to use the test actions instead of the default actions.  Maybe now you’re screaming, “Dear God, first you try to convince me that taking a dependency on the web server isn’t an integration test, and now you want to put test code in production!” Well, you shouldn’t scream like that, you’re scaring the neighbors. Its just code, and we’ll take steps later to isolate this code so it can be easily excluded during Release builds.

Now that you’re done screaming, lets work our way inward until we’ve implemented the scenario described above. First, we’ll update our test so that it points to our unwritten test action in the MVC project.

[TestMethod]
public void TestIndexView()
{
  MvcApprovals.VerifyMvcPage(new HomeController().TestIndex);
}

Now we can create this method on our HomeController using our favorite refactoring tool. Before we can implement this action, we need to create a seam inside HomeController. Lets think about what the default Index method does:

public ActionResult Index()
{
  var cars = repository.GetCars();
  return View(cars);
}

Looks to me like TestIndex needs to do two things:

public ActionResult TestIndex()
{
  // Get the data
  // Pass data to the view.
}

Of these two parts, one part is the same between test and production, and one part varies. We need to separate the part that varies from the part that stays the same. In our simple scenario, that means moving the part that stays the same “Pass data to the view” into its own method so that both the production method and the test method can do what they want with the part that changes “Get the data”.

Here is the new Index method and the seam:

public ActionResult Index()
{
  var cars = repository.GetCars();
  return Index(cars);
} 
private ActionResult Index(IEnumerable<ICar> cars)
{
  return View(cars);
}

For reasons that will become apparent, the private seam must have the same name as the MVC action. We make the seam private to avoid ambiguity when MVC tries to resolve the route to the Index method.

We can create any model that we want in TestIndex, and pass it to the view using the private seam. The repository is not required inside the seam so we don’t care if it throws exceptions. In the test action we could use fakes or mocks, but we don’t even need to. We have access to our concrete models, and we can just insatiate them with any data we want. In this example, our Car implementation is just an ordinary class, but you can just as easily create EntityFramework or LINQ to SQL objects.

Before getting into that, lets isolate our test code by turning HomeController into a partial class and putting TestIndex into another file. Create HomeController.Tests.cs in your Controllers folder and put TestIndex in there:

public partial class HomeController
{
  public ActionResult TestIndex()
  {
    // Create data
    // Pass to view
    return null;
  }
}

Now lets create some test data and pass it to the seam.

public ActionResult TestIndex()
{
    // Create data
    IEnumerable<ICar> cars = new Car[] {
        new Car() { Make = "VW", Model = "Passat", Year = 2002 },
        new Car() { Make = "Honda", Model = "Fit", Year = 2007 },
    };

    // Pass to view
    return Index(cars);
}

That should be it. Our test should pass. But now its failing with a new error:

mvcviewtest5_thumb8

MVC is looking for a View called “TestIndex” even though our seam is called Index. The view name inference depends on the view engine implementation.  In the WebFormsViewEngine, the view name seems to be inferred from the virtual path as near as I can tell.  After reading the source code for awhile my brain started to hurt.  Anyway, whether MVC is inferring the data from the path, or getting it from the route data, we can override the behavior.  We’ll give MVC the view name using an overload that allows us to specify the name explicitly:

private ActionResult Index(IEnumerable<ICar> cars)
{
  return View("Index", cars);
}

This introduces a magic string into our code and we’d like to avoid that.  We can do so by taking advantage of a convenience method provided by the ApprovalTests companion library, ApprovalUtilities. We need to add a reference to ApprovalUtilities to our MVC project.  The easiest way to do that is to use NuGet.  If we use NuGet we will also get a reference to ApprovalTests.  If having a reference to ApprovalTests in your MVC project bothers you, don’t use NuGet.  You can always add the reference by hand if you like.   I’m going to use NuGet.

After adding the reference, import this namespace:

using ApprovalUtilities.Asp.Mvc;

Then update the private Index method again:

private ActionResult Index(IEnumerable<ICar> cars)
{
  return View(cars).Explicit();
}

This extension method is functionally equivalent to the magic-string version.

Note for NCrunch Users

Update 2012/06/18: You can skip this!  NCrunch v1.40b resolved this issue.  Cheers!

There is an issue using this extension method in the current version of NCrunch. On two of my machines, NCrunch will fail the Controller test (not the View test) when run with the Explicit method. I get this exception:

System.MissingMethodException: Method not found: ‘System.Web.Mvc.ViewResult ApprovalUtilities.Asp.Mvc.MvcUtilites.Explicit(System.Web.Mvc.ViewResult)’.

The documentation for MissingMethodException indicates that the exception is thrown when you try to dynamically access a method that does not exist.  For example, you use reflection to access a type, but then call InvokeMember with the name of a method that doesn’t exist.  I’ve looked at the assemblies generated by NCrunch, both with and without instrumentation, and its not trying to do this.  So I don’t know why we get this exception.  This occurs whether called as an extension method or as a regular static method call. This occurs in both Debug and Release configurations. This occurs when compiled for Any CPU and when compiled for x86. This occurs under MSTest and under xUnit.  It happens in 1.38b and 1.39b.

Researching this, I could only find one other person who looked like they had an issue that was remotely similar, but the similarity was very remote indeed and he hadn’t found a resolution yet. So for the time being I’m working around this issue by checking for NCrunch at compile time:

  private ActionResult Index(IEnumerable<ICar> cars)
  {
#if NCRUNCH
    return View("Index", cars);
#else
    return View(cars).Explicit();
#endif
  }

There are two other alternatives. First we can give up on using the extension method, and second we can give up on NCrunch. I don’t think we need to give up either. NCrunch is a great tool, but its still in beta. This is the first real issue I’ve had with it since the March 2012 release. I’m hopeful that this issue can be resolved with further research, or in a future version of the tool . This compile time condition will remind me to test this with NCrunch the next time an update is released, and to keep looking for an answer online.  When this issue is resolved I can get rid of the branch that uses the magic string.

Update 2012-05-18: 

I decided it might be a good idea to report this issue to the NCrunch developer, Remco Mulder, over on the NCrunch Forums.  We we were able to isolate the cause of the exception.  Our example site is built against MVC 3 and ApprovalUtilities is built against MVC 2.  The current version of NCrunch seems to be incorrectly loading both MVC assemblies into its test environment, creating confusion over what version of ViewResult is valid for use with Explicit().  The good news is that the problem only occurs if you have MVC 2 in your GAC.  I still need it because I have some sites that are built against MVC 2, but if you don’t have MVC 2 on your machine, then you probably wont see this problem at all, and you won’t need to use this workaround.

Back to Our Story…

Now that we’ve explicitly specified the view’s name (one way or another) we can run the tests again. This time we get an approval failure and ApprovalTests launches the browser to show us the received file. Great Success!

mvcviewtest6_thumb2

This page may not be the epitome of good design or usability, and maybe we want to customize it further, but at the moment we can see our test data in the page, and that’s awesome. If we do want to customize, we can do so with feedback. When we finally get the page looking the way we would like, we can approve the output and achieve protection from regressions.

At this point we have a few tests. We have an original test generated by Visual Studio, and we replaced one generated test with one that now exercises the default Index method with a mock. We have our two View tests which use MvcApprovals, and one of these uses seams to test the Index with fake data. We also have a couple of tests on the Models which I haven’t mentioned until now, because they are not very interesting. The only failing test is the Index view test, and it passes after I approve the output.

The tests are green in two build environments, Visual Studio and NCrunch. Seems like a good time for a commit, followed by some housekeeping.

Housekeeping

This housekeeping round is relatively minor. We want to make sure that our test actions are not available when we compile in Release mode. This is optional, but probably a good idea.

We can just eliminate half of our partial class using a compiler condition:

#if DEBUG
namespace MVCTestSite.Controllers
{
  public partial class HomeController
  {
    public ActionResult TestIndex()
    {
      // Create data
      IEnumerable<ICar> cars = new Car[] {
        new Car() { Make = "VW", Model = "Passat", Year = 2002 },
        new Car() { Make = "Honda", Model = "Fit", Year = 2007 },
      };

      // Pass to view
      return Index(cars);
    }
  }
}
#endif

Since our test action will no longer be available in release mode, we should also eliminate our “test pointer” when compiling in release mode:

namespace MVCTestSite.Tests.Views
{
  [TestClass]
  [UseReporter(typeof(FileLauncherReporter), typeof(ClipboardReporter))]
  public class HomeViewsTest : MvcTest
  {
#if DEBUG
    [TestMethod]
    public void TestIndexView()
    {
      MvcApprovals.VerifyMvcPage(new HomeController().TestIndex);
    }
#endif

    [TestMethod]
    public void TestAboutView()
    {
      MvcApprovals.VerifyMvcPage(new HomeController().About);
    }
  }
}

We can leave the test on the About view outside the conditional, since it does not require any non-production actions.

Since we are still in Debug mode, these conditions remain true and our tests still run and pass. There is another factor to consider when our project is compiled in Release mode and that is optimization. Our friend the Explicit() method infers the view name by examining the name of the method that calls it. This is why we needed to make sure that our private Index method had the same name as the public method. However, the default configuration for Release mode includes optimizations which could cause all of our action methods to be in-lined out of existence. This changes the method that calls Explicit. In my case, an anonymous delegate ended up calling Explicit and MVC complained that there was no view called “lambda_method”. Oops.

Llewellyn’s video shows how to work around this issue by disabling optimization in the Release configuration. If that idea bothers you, there is another way to do it on a method-by-method basis using an attribute:

[MethodImpl(MethodImplOptions.NoInlining)]
private ActionResult Index(IEnumerable<ICar> cars)
{
  return View(cars).Explicit();
}

Both work, but you need to pick one of these or you risk the “View Not Found” error.

Now that we have seen how to work with data, we can use these techniques continue to develop our site.  We can use ApprovalTests or traditional Asserts to work with our Models and Controllers, and we now have the ability to use MvcApprovals with our views.

Up until now, we’ve been working in two build environments, Visual Studio and NCrunch.  Although these environments are different, they share a lot of environmental factors because they both run on the same host.  To help ensure that our tests are robust, we should get them running on a different host with a different set of rules.  In the last part of this series, ApprovalTests and MVC Views: Tackling the Build Server, I’ll try to get these tests running under CC.NET.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s