Tips and Tricks for Unit Testing Your ASP.NET MVC Application

Now that you have the necessary tools in your belt, let's take a closer look at some of the more common unit testing tasks in ASP.NET MVC applications.

Testing Controllers

The default unit test project already includes some controller tests (which you modified earlier in this chapter). A surprising number of subtleties are involved with testing controllers, and as with all things, the subtleties between decent and great code can often be found in small differences.

Keep Business Logic out of Your Controllers

The primary purpose of a controller in a Model-View-Controller architecture is to be the coordinator between the model (where your business logic lives) and the view (where your user interface lives). The controller is the dispatcher that wires everybody together and gets everybody running.

When we talk about business logic, it could be something as simple as data or input validation, or something as complex as applying long-running processes like core business workflow. As an example, controllers shouldn't try to validate that models are correct; that is the purpose of the business model layer. It does, however, need to concern itself with what actions to take when it has been told that the model isn't valid (perhaps re-displaying a particular view when it's invalid, or sending the user off to another page when the model is valid).

Because your controller action methods will be relatively simple, the unit tests for your action methods should also be correspondingly simple. You also want to try to keep business knowledge out of the unit test, just as you could out of the controllers.

To make this advice concrete, consider the case of models and validation. The differences between a good unit test and a bad one can be fairly subtle. A good unit test would provide a fake business logic layer that tells the controller that the model is valid (or not) based on the needs of the test; a bad unit test would cobble together good or bad data and let the existing business logic layer tell the controller whether it's good or bad. The bad unit test is testing two components at once (the controller action and the business layer). A less obvious problem with the bad unit test, though, is that it has baked into it the knowledge of what bad data actually is; if the definition of bad data changes over time, then the test becomes broken, perhaps causing a false negative (or worse, a false positive) when running the test.

Writing the good unit test requires a little more discipline in the design of the controller, which leads directly to my second piece of advice.

Pass Service Dependencies via Constructor

To write the good unit test just discussed, you need to substitute in a fake business layer. If the controller has a direct tie into the business layer, this can be quite challenging. If, on the other hand, it takes the business layer as a service parameter via the constructor, it becomes trivial for you to provide the fake.

This is where the advice of Chapter 11 can really shine. ASP.NET MVC 3 introduced some simple ways to enable dependency injection in your application, making it not only possible but trivial to support the idea of getting services via constructor parameters. You can now leverage that work very easily in your unit tests, to help test in isolation (one of our three critical aspects of unit testing).

To test these service dependencies, the services need to be replaceable. Usually that means you need to express your services in terms of interfaces or abstract base classes. The fake substitutes that you write for your unit tests can be handwritten implementations, or you can use a mocking framework to simplify the implementation for you. There are even special kinds of dependency injection containers called auto-mocking containers that automatically create the implementations as needed.

A common practice for handwriting a fake service is called a spy, which simply records the values that it is passed so that it can later be inspected by the unit test. For example, assume that you have a math service (a trivial example, I know) with the following interface:

public interface IMathService
{
    int Add(int left, int right);
}

The method in question takes two values and returns one. The real implementation of math service is obviously going to add the two values together. The spy implementation might look something like this:

public class SpyMathService : IMathService
{
    public int Add_Left;
    public int Add_Right;
    public int Add_Result;

    public int Add(int left, int right)
    {
        Add_Left = left;
        Add_Right = right;
        return Add_Result;
    }
}

Now your unit test can create an instance of this spy, set Add_Result with the value that it wants passed back when Add is called, and after the test is complete, it can make assertions on the Add_Left and Add_Right values, to ensure that correct interaction happened. Notice that our spy doesn't add the values together; we're only concerned with the values going into and out of the math service:

[TestMethod]
public void ControllerUsesMathService()
{
    var service = new SpyMathService { Add_Result = 42; }
    var controller = new AdditionController(service);

    var result = controller.Calculate(4, 12);

    Assert.AreEqual(service.Add_Result, result.ViewBag.TotalCount);
    Assert.AreEqual(4, service.Add_Left);
    Assert.AreEqual(12, service.Add_Right);
}

Favor Action Results over HttpContext Manipulation

You can think of the ASP.NET core infrastructure as the IHttpModule and IHttpHandler interfaces, plus the HttpContext hierarchy of classes (HttpRequest, HttpResponse, and so on). These are the fundamental underlying classes that all ASP.NET is built upon, whether that means Web Forms, MVC, or Web Pages.

Unfortunately these classes aren't very test-friendly. There is no way to replace their functionality, which makes testing any interactions with them very difficult (although not impossible). .NET 3.5 SP1 introduced an assembly named System.Web.Abstractions.dll, which created abstract class versions of these classes (HttpContextBase is the abstract version of HttpContext). Everything in MVC is written against these abstract classes instead of their original counterparts, and it makes testing code that interacts with these classes much easier.

It's not perfect, though. These classes still have very deep hierarchies, and most of them have dozens of properties and methods. Providing spy versions of these classes can be very tedious and error-prone, so most developers resort to mocking frameworks to make the work easier. Even so, setting up the mocking frameworks can be tedious and repetitive work. Controller tests are going to be numerous, so you want to minimize the pain involved in writing them.

Consider the RedirectResult class in MVC. The implementation of this class is fairly straightforward: it just calls HttpContextBase.Response.Redirect on your behalf. Why did the team go to all the trouble to create this class, when you're trading one line of code for another (slightly simpler) line of code? The answer is: to make unit testing easier.

To illustrate, write a hypothetical action method that does nothing but redirect you to another part of the site:

public void SendMeSomewhereElse()
{
    Response.Redirect("∼/Some/Other/Place");
}

This action is fairly straightforward to understand, but the test is a lot less straightforward than we'd like. Using the Moq mocking framework (available at http://code.google.com/p/moq/), your unit test might look like this:

[TestMethod]
public void SendMeSomewhereElseIssuesRedirect()
{
    var mockContext = new Mock<ControllerContext>();
    mockContext.Setup(c =>
        c.HttpContext.Response.Redirect("∼/Some/Other/Place"));
    var controller = new HomeController();
    controller.ControllerContext = mockContext.Object;

    controller.SendMeSomewhereElse();

    mockContext.Verify();
}

That's a couple extra ugly lines of code, even after you figure out how to write them! Redirect is probably one of the simplest things you can do, too. Imagine that you had to write code like this every time you wanted to write a test for an action. Believe me when I say that the source listing for the necessary spy classes would take several pages, so Moq is actually pretty close to the ideal situation for the test. However, with a small change, the controller reads roughly the same, but the unit test becomes much more readable:

public RedirectResult SendMeSomewhereElse()
{
    return Redirect("∼/Some/Other/Place");
}

[TestMethod]
public void SendMeSomewhereElseIssuesRedirect()
{
    var controller = new HomeController();

    var result = controller.SendMeSomewhereElse();

    Assert.AreEqual("∼/Some/Other/Place", result.Url);
}

When you encapsulate your interactions with HttpContext (and friends) inside of an action result, you're moving the testing burden to a single isolated place. All your controllers can reap the benefit of much more readable tests for themselves. Just as important, if you need to change the logic, you have a single place to change it (and only a handful of tests to change, instead of needing to change dozens or hundreds of controller tests).

Favor Action Parameters over UpdateModel

The model binding system in ASP.NET MVC is what is responsible for translating request data into values that your actions can use. That request data might come from form posts, from query string values, and even from parts of the path of the URL. No matter where that data comes from, though, there are two common ways to get it in your controller: as an action parameter, and by calling UpdateModel (or its slightly wordier sibling TryUpdateModel).

Here is an example of an action method using both techniques:

[HttpPost]
public ActionResult Edit(int id)
{
    Person person = new Person();
    UpdateModel(person);

    [...other code left out for clarity...]
}

The id parameter and the person variable are using the two aforementioned techniques. The unit testing benefit to using the action parameter should be obvious: It's trivial for the unit test to provide an instance of whatever type your action method needs, and there is no need to change any of the infrastructure to make it happen. UpdateModel, on the other hand, is a non-virtual method on the Controller base class, which means that you cannot easily override its behavior.

If you truly need to update UpdateModel, you have several strategies to feed your own data to the model binding system. The most obvious is overriding ControllerContext (as shown in the previous section “Favor Action Results over HttpContext Manipulation”), and providing fake form data for the model binders to consume. The Controller class also has ways to provide model binders and/or value providers that can be used to provide the fake data. It should be clear from our exploration of mocking, though, that these options are a last resort.

Utilize Action Filters for Orthogonal Activities

This piece of advice is similar to the one about action results. The core recommendation is to isolate code that might be harder to test into a reusable unit, so the difficult testing becomes tied up with that reusable unit, and not spread all throughout your controller tests.

That doesn't mean you have no unit testing burden, though. Unlike the action result situation, you don't have any input or output that you can directly inspect. An action filter is usually applied to an action method or a controller class. In order to unit test this, you merely need to ensure that the attribute is present, and leave testing the actual functionality to someone else. Your unit test can use some simple reflection to find and verify the existence of the attribute (and any important parameters you want to check).

An important aspect of action filters, though, is that they don't run when your unit tests invoke the actions. The reason action filters do their work in a normal MVC application is because the MVC framework itself is responsible for finding them and running them at the right time. There is no “magic” in these attributes that makes them run just because the method they're attached to is running.

When you're running actions in your unit tests, remember that you cannot rely on the action filters executing. This may slightly complicate the logic in the action method, depending on what the action filter does. If the filter adds data to the ViewBag property, for example, that data is not present when the action runs under the unit test. You need to be conscious of that fact both in the unit tests and in the controller itself.

The advice in this section's title recommends action filters should be limited to orthogonal activities precisely because the action filter doesn't run in the unit test environment. If the action filter is doing something that's critical for the execution of the action, your code probably belongs somewhere else (like a helper class instead of a filter attribute).

Testing Routes

Testing routes tends to be a fairly straightforward process once you've figured out all the bits of infrastructure that need to be in place. Because routing uses the core ASP.NET infrastructure, you'll rely on Moq to write the replacements.

The default MVC project template registers two routes inside of your global.asax file:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        "Default",
        "{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

It's very convenient that the MVC tooling created this function as a public static function. This means you can very easily call this from your unit test with an instance of RouteCollection and get it to map all of your routes into the collection for easy inspection and execution.

Before you can test this code, you need to understand a little bit about the routing system. Some of this was covered in Chapter 9, but the part that's important for you to understand now is how the underlying route registration system works. If you examine the Add method on RouteCollection, you'll see that it takes a name and an instance of the RouteBase type:

public void Add(string name, RouteBase item)

The RouteBase class is abstract, and its primary purpose is to map incoming request data into route data:

public abstract RouteData GetRouteData(HttpContextBase httpContext)

MVC applications don't generally use the Add method directly; instead, they call the MapRoute method (an extension method provided by the MVC framework). Inside the body of MapRoute, the MVC framework itself does the work of calling Add with an appropriate RouteBase object. For your purposes, you really only care about the RouteData result; specifically, you want to know which handler is invoked, and what the resulting route data values are.

Testing Calls to IgnoreRoute

You'll start with the call to IgnoreRoute, and write a test that shows it in action:

[TestMethod]
public void RouteForEmbeddedResource()
{
    // Arrange
    var mockContext = new Mock<HttpContextBase>();
    mockContext.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath)
               .Returns("∼/handler.axd");
    var routes = new RouteCollection();
    MvcApplication.RegisterRoutes(routes);

    // Act
    RouteData routeData = routes.GetRouteData(mockContext.Object);

    // Assert
    Assert.IsNotNull(routeData);
    Assert.IsInstanceOfType(routeData.RouteHandler,
                            typeof(StopRoutingHandler));
}

The arrange section creates a mock of the HttpContextBase type. Routing only needs to know what the request URL is, and to do that, it calls Request.AppRelativeCurrentExecutionFilePath. All you need to do is tell Moq to return whatever URL you want to test whenever routing calls that method. The rest of the arrange section creates an empty route collection, and asks the application to register its routes into the collection.

The act line then asks the routes to act on the request and tell you what the resulting RouteData is. If there were no matching routes, the RouteData instance will be null, so your first test is to ensure that you did match some route. For this test, you don't care about any of the route data values; the only thing that's important is for you to know that you hit an ignore route, and you know that because the route handler will be an instance of System.Web.Routing.StopRoutingHandler.

Testing Calls to MapRoute

It's probably more interesting to test calls to MapRoute because these are the routes that actually match up with your application functionality. Though you only have one route by default, you have several incoming URLs that might match this route.

Your first test ensures that incoming requests for the homepage map to your default controller and action:

[TestMethod]
public void RouteToHomePage()
{
    var mockContext = new Mock<HttpContextBase>();
    mockContext.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath)
                .Returns("∼/");
    var routes = new RouteCollection();
    MvcApplication.RegisterRoutes(routes);

    RouteData routeData = routes.GetRouteData(mockContext.Object);

    Assert.IsNotNull(routeData);
    Assert.AreEqual("Home", routeData.Values["controller"]);
    Assert.AreEqual("Index", routeData.Values["action"]);
    Assert.AreEqual(UrlParameter.Optional, routeData.Values["id"]);
}

Unlike the ignore route tests, in this test you want to know what values are going inside of your route data. The values for controller, action, and id are filled in by the routing system. Because you have three replaceable parts to this route, you'll end up with four tests that probably have data and results like those in Table 12.1. If your unit testing framework supports data-driven tests, routes are an excellent place to take advantage of such features.

Table 12.1: Default Route Mapping Examples

images/c12tnt001.jpg

Testing Unmatched Routes

Don't. Seriously, just don't. The tests you've written up until now were tests of code that we wrote; namely, calls to IgnoreRoute or MapRoute. If we write a test for unmatched routes, we're just testing the routing system at that point. We can assume that just works.

Testing Validators

The validation system in ASP.NET MVC 3 has changed in several very important ways, many of which were discussed in Chapter 6. On the server side, MVC 3's minimum platform target moved up to .NET 4. This means developers can take advantage of improvements in Data Annotations, including support for the IValidatableObject interface and the new context-based validation override of ValidationAttribute.IsValid. A new interface was also added to MVC (IClientValidatable) to make it easier for validation attributes to participate in client-side validation. Although .NET 4 has no new validation attributes, two new ones were added to MVC itself: CompareAttribute and RemoteAttribute.

On the client side, the changes are more dramatic. The MVC team added support for unobtrusive validation, which renders the validation rules as HTML elements instead of inline JavaScript code. In addition, MVC 3 is the first version that delivered on the ASP.NET team's commitment to fully embrace the jQuery family of JavaScript frameworks. The unobtrusive validation feature is implemented in a framework-independent manner, but the implementation shipped with MVC is based on jQuery 1.4.4 and jQuery Validate 1.7.

It is common for developers to want to write new validation rules, and most will quickly outgrow the four built-in validation rules (Required, Range, RegularExpression, and StringLength). At a minimum, writing a validation rule means writing the server-side validation code, which you can test with server-side unit testing frameworks. Additionally, you can use server-side unit testing frameworks to test the client-side metadata API in IClientValidatable to ensure that the rule is emitting the correct client-side rule. Writing tests for both these pieces should be relatively straightforward, once you're familiar with how the Data Annotations validation system works.

Client-Side (JavaScript) Unit Testing

If there is no corresponding client-side rule that's a reasonable match for the validation rule, the developer may also choose to write a small piece of JavaScript, which can be unit tested using a client-side unit testing framework (like QUnit, the unit testing framework developed by the jQuery team). Writing unit tests for client-side JavaScript is beyond the scope of this chapter. I strongly encourage developers to invest time in finding a good client-side unit testing system for their JavaScript code.

A validation attribute derives from the ValidationAttribute base class, from System.ComponentModel.DataAnnotations. Implementing validation logic means overriding one of the two IsValid methods. You might recall the maximum words validator from Chapter 6, which started out like this:

public class MaxWordsAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(
        object value, ValidationContext validationContext)
    {
        return ValidationResult.Success;
    }
}

This validator attribute has the validation context passed to it as a parameter. This is the new overload available in the data annotations library in .NET 4. You could also override the version of IsValid from the original .NET 3.5 data annotations validation API:

public class MaxWordsAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        return true;
    }
}

Which API you choose to override really depends on whether or you not you want/need access to the validation context. The validation context gives you the ability to interact with the container object that your value is residing inside of. This is an issue when you consider unit testing, because any validator which uses information inside of the validation context is going to need to get a validation context provided to it. If your validator overrides the version of IsValid which does not take a validation context, then you can call the version of Validate on it which only requires the model value and the parameter name.

On the other hand, if you implement the version of IsValid which includes the validation context (and you need values from that validation context), then you must call the version of Validate which includes the validation context; otherwise, the validation context will be null inside of IsValid. Theoretically, any implementation of IsValid must be resilient when being called without a validation context, since it might be called by code that was written against the .NET 3.5 data annotations API; in practice, though, any validator which is used only in MVC 3 or later can safely assume that it will always be getting a validation context.

This means when you write your unit tests, you will need to provide a validation context to your validators (at the very least when you know those validators will be using one, but in practice, you might as well always do the right thing and provide the validation context).

Correctly creating the ValidationContext object can be tricky. There are several members you need to set correctly so that it can be consumed properly by the validator. The ValidationContext takes three arguments to its constructor: the model instance that's being validated, the service container, and the items collection. Of these three parameters, only the model instance is required; the others should be null because they are unused in ASP.NET MVC applications.

MVC does two different types of validation: model-level validation and property-level validation. Model-level validation is performed when the model object as a whole is being validated (that is, the validation attribute is placed on the class itself); property-level validation is performed when validating a single property of the model (that is, the validation attribute is placed on a property inside the model class). The ValidationContext object is set up differently in each scenario.

When performing model-level validation, the unit test sets up the ValidationContext object as shown in Table 12.2; when performing property-level validation, the unit test uses the rules shown in Table 12.3.

Table 12.2: Validation Context for Model Validation

Property What it Should Contain
DisplayName This property is used in error messages, replacing the {0} replacement token. For Model Validation, it is usually the simple name of the type (that is, the class name without the namespace prefix).
Items This property isn't used in ASP.NET MVC applications.
MemberName This property isn't used in Model Validation.
ObjectInstance This property is the value passed to the constructor, and should be the instance of the model that's being validated. Note that this is the same value you will be passing to Validate.
ObjectType This is the type of the model being validated. This is automatically set for you to match the type of the object passed into the ValidationContext constructor.
ServiceContainer This value isn't used in ASP.NET MVC applications.

Table 12.3: Validation Context for Property Validation

Property What it Should Contain
DisplayName This property is used in error messages, replacing the {0} replacement token. For Property Validation, it is usually the name of the property, although that name may be influenced by attributes like [Display] or [DisplayName].
Items This property isn't used in ASP.NET MVC applications.
MemberName This property should contain the actual property name of the property being validated. Unlike DisplayName, which is used for display purposes, this should be the exact property name as it appears in the model class.
ObjectInstance This property is the value passed to the constructor, and should be in the instance of the model that contains the property being validated. Unlike in the case of Model Validation, this value is not the same value that you will be passing to Validate (that will be the value of property).
ObjectType This is the type of the model being validated (not the type of the property). This is automatically set for you to match the type of the object passed into the ValidationContext constructor.
ServiceContainer This property isn't used in ASP.NET MVC applications.

Let's take a look at some sample code for each scenario. The following code shows how you would initialize the validation context to unit test model-level validation (assuming you were testing an instance of a hypothetical class named ModelClass):

var model = new ModelClass { /* initialize properties here */ };
var context = new ValidationContext(model, null, null) {
    DisplayName = model.GetType().Name
};
var validator = new ValidationAttributeUnderTest();

validator.Validate(model, context);

Inside the test, the call to Validate will throw an instance of the ValidationException class if there were any validation errors. When you're expecting the validation to fail, surround the call to Validate with a try/catch block, or use your test framework's preferred method for testing for exceptions.

Now let's show what the code might look like to test property level validation. If we were testing a property named FirstName on your ModelClass model, the test code might look something like this:

var model = new ModelClass { FirstName = "Brad" };
var context = new ValidationContext(model, null, null) {
    DisplayName = "The First Name",
    MemberName = "FirstName"
};
var validator = new ValidationAttributeUnderTest();

validator.Validate(model.FirstName, context);

Comparing this code to the previous example, there are two key differences.

  • First, the code sets the value of MemberName to match the property name, whereas model-level validation sample didn't set any value for MemberName.
  • Second, we pass the value of the property we're testing when we call Validate, whereas in the model-level validation sample we passed the value of the model itself to Validate.

Of course, all this code is only necessary if you know that your validation attribute requires access to the validation context. If you know that the attribute doesn't need validation context information, then you can use the simpler Validate method which only takes the object value and the display name (these two values match the value you're passing to the ValidationContext constructor and the value you're setting into the DisplayName property of the validation context, respectively).

Product Team Aside: Additional Validation Attributes in MVC Futures

The MVC team shipped several additional validation attributes in the MVC Futures package. Additional validation attributes are available for most of the rules built into jQuery Validate 1.7, including server-side implementations that match the client-side code (for example, using the same regular expressions for things like e-mail and URL validation).

Look for these new attributes in the MVC 3 Futures package:

  • CreditCardAttribute
  • EmailAddressAttribute
  • FileExtensionsAttribute
  • UrlAttribute

You can get MVC 3 Futures from NuGet by installing the Mvc3Futures package.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.78.102