Yan-Feng

记录经历、收藏经典、分享经验

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Overview

Before we start diving into Test Driven Development (commonly referred to by its acronym, TDD), the authors need to make one thing clear. ASP.NET MVC is not solely for those who practice Test Driven Development. So if you don't practice TDD and planned to dismiss this chapter, please stick around for just one small moment. We're not going to try to convert you to TDD or be preachy about it (though Phil might if you happen to run into him on a street corner), but do give us a chance to explain why Microsoft's efforts to make this framework friendly to TDD fans benefits you, even if you're opposed to TDD.

So why is there all this focus on TDD when it comes to ASP.NET MVC? To understand the answer, it helps to have a bit of historical perspective. ASP.NET wasn't originally designed with TDD practitioners in mind. At the time, TDD was in its infancy and not as widely adopted as it is today. As a result, there are many areas of ASP.NET that provide challenges to those trying to write automated unit tests for developers making use of those areas because they are tightly coupled with other subsystems.

A framework designed with testability in mind has more benefits than just being able to write unit tests. Such a framework is extremely extensible as a byproduct of its being testable, since to write a proper unit test often requires isolating pieces of the framework and swapping out other dependencies that the framework uses with test doubles — fake implementations of an interface under your full control.

As it turns out, TDD practitioners have high demands when it comes to testability. TDD is a code design activity that produces unit tests and thus requires the underlying framework to be inherently testable. There are techniques for working around areas that are not testable, but a framework that requires too many of these workarounds produces a lot of friction for the TDD practitioner. And friction in this case, makes people unhappy.

If the ASP.NET MVC Framework can keep this friction to a minimum, then it's not only the TDD practitioners who benefit, it is all developers who need to use and extend the framework. This is the reason for all the fuss around TDD and ASP.NET MVC.

A Brief Introduction to TDD

Test Driven Development is unfortunately a misnomer, but one that has stuck around. Much as the Great Dane is not Danish and french fries are not from France, Test Driven Development is not about testing. Nothing against testers (some of the authors' best friends are testers), but TDD is not a quality assurance (QA) activity. Unfortunately, this is a great source of confusion for many developers upon first approaching TDD. And who can blame them? You've got "Test" right there as the first word in the acronym!

TDD is a code design activity that employs the writing of automated unit tests to help shape the design of code. These tests are better thought of as executable specifications. Another way to think of TDD is that it is Design-by-Example, in which the "tests" really serve as examples of how client code would use the given API.

Another term some people are seeking to supplant TDD with is Behavioral Driven Development, which is known by yet another three-letter acronym, BDD. BDD is more than just a renaming of the practices of Test Driven Development. It is an attempt to refine the practice of TDD to use language that is more closely tuned to the domain when developing the specifications. It is too early to tell if TDD is already too ingrained for another term to supplant it.

Because TDD is not a QA activity, it is not a replacement for other types of tests. You might hear TDD proponents suggest that a unit test should not touch the database and find yourself wondering, "Well then, how do I write a test that calls a method and ensure that the right data is retrieved from the database?"

That is certainly a valuable test to perform, and even automate, to ensure that your code accesses the database correctly. But from a TDD perspective, that would be considered an integration test between your code and the database, not a unit test. Notice that this code is focused on correct behavior and integration with the database and not on the design of the code, which TDD focuses on.

What Does TDD Look Like?

The practice of TDD has a simple rhythmic approach to it:

  • First write a unit test that fails.

  • Write just enough code to make the test pass.

  • Refactor the code as necessary (remove duplication, etc.).

  • Repeat.

Write a Unit Test That Fails

A tricky part for those new to TDD is taking the first step in which you write a test before you've written the unit of code you are testing. This forces you to think a bit about the behavior of the code before you write it. It also forces you to consider the interface of the code. How will another developer make use of the code you are writing?

When faced with an empty test code file, start with the simplest test. For beginners to TDD, this might even be as simple as writing a test to make sure that you can construct the type. For example, when testing a new method, the authors sometimes find it helpful to begin with testing exception cases for the arguments of the method. For example, you might write a test to make sure that when you pass null for an argument, that an ArgumentNullException is thrown. It's not exactly the most interesting behavior to start specifying, but in many cases, the physical act of writing this simple test helps to jump-start the brain into thinking through the more interesting behaviors of the code, and the tests will start flowing from your fingers. It's a great way to break "developer's block" (our version of "writer's block").

Many TDD practitioners feel it's important to start with the "essence" of the code you are testing. Using a dirt simple example, let's suppose that you need to write a method that counts the occurrences of a specific character in a string. The essence of that method is that it counts characters so you might start off with a test that demonstrates expected correct behavior:

[Test]public void StringWithThreeAsReturnsThreeWhenCountingOccurencesOfA() {  //arrange  CharCounter counter = new CharCounter();  //act  int occurrences =    counter.CountOccurrences("this phrase has three occurences of a.", ‘a');  //assert  Assert.AreEqual(3, occurrences);}

Note that we haven't even written the code yet. This test helps to inform us what the design of the API to count characters should be.

Write Just Enough Code to Make the Test Pass

Next, you write just enough code to make the test pass and no more. This can be a challenging discipline as the temptation to skip ahead and implement the whole method/class/module is great. We all fall prey to it at one time or another. However, if you're practicing TDD, you really want to avoid writing code that doesn't have a test already written for it.

The test is really a justification for why you need that piece of code in the first place. This makes sense when you think of a test as a specification. In this case, you want to avoid writing code that doesn't have a specification written for it. With TDD, that specification is in the form of an automated executable test.

When you feel the temptation to write more code than necessary to make the test pass, invoke the acronym YAGNI: YOU AIN'T GONNA NEED IT! Keeping YAGNI in mind is a great means of avoiding premature generalization and gold plating, in which you make your code more complex than necessary to handle all sorts of scenarios that you may never actually encounter. Code that is never executed is more prone to misunderstanding and bugs and should be trimmed from the code base. If you have a test written, at least you have one consumer of that piece of code, the test.

While code coverage is not the goal of writing unit tests, running a code coverage tool comes in handy when you run your unit tests. You can use the coverage report to find code without tests and use it as a guide to determine whether or not you actually need the code. If you do, then write some tests; otherwise delete the code.

In this case, you could have the method simply return 3, which makes this test pass, but that's just being cheeky. Let's implement the method:

public int CountOccurrences(string text, char searchCharacter){    int count = 0;    foreach (char character in text) {        if (character == searchCharacter) {            count++;        }    }    return count;}

Refactor the Code

Now, you're ready to refactor the method, if necessary, removing any duplicate code, and so forth. All systems of code experience the phenomena of entropy over time. Sometimes, in the rush of deadlines, that entropy is introduced by taking shortcuts, which incur technical debt.

You've probably run into this before: "I know I should combine these two similar methods into one, but I've got to get this done in the next half-hour so I'll just copy the method, tweak a few things, and check it in. I can fix it up later."

That right there is incurring technical debt. The question is when do you return to pay down the debt by fixing the code? When applying TDD, you do that right after you make a test pass and then vigorously remove any duplicate code. Try to clean up any sloppy implementations that pass the tests but may be difficult to understand. You're not changing the behavior of the code; you're merely changing the structure of the code to be more readable and maintainable.

When you refactor, you start out by running all of your tests and should only proceed refactoring if they are all green (aka passing). Resist the temptation to make changes to your test as you make changes to the code because this probably indicates that you're changing the behavior of your code. Refactoring should be a process in which your tests are always in a passing state as you make small changes to improve the implementation.

The beauty of this approach is that when you are done with the refactoring, your tests provide strong confidence that your refactoring changes haven't changed the behavior of code.

Repeat

If your code meets all its requirements, you move on to the next unit of code and start this process all over again. However, if you've identified another behavior that the unit of code must have, it's time to write another test for that same unit of code.

Looking at this simple example, you know you're not done yet. There are other requirements you haven't implemented. For example, what if the text passed in is null, what should the method do? You should now start the process over by writing a test that specifies the expected behavior, then implementing the code to make that test pass.

This iterative process provides immediate feedback for the design and ensures the correctness of the code you are writing — in contrast to writing a bunch of code and then testing it all at once.

Writing Good Unit Tests

To get maximum benefit from writing unit tests, there are a couple of key principles to keep in mind while writing tests. Actually, there are many principles and qualities of good unit tests, but understanding the two mentioned here will give you insight into why the ASP.NET MVC team made certain framework design decisions.

Tests Should Not Cross Boundaries

As those familiar with TDD know, the principle here is that a unit test should test the code at the unit level and should not reach across system boundaries. The boundary is typically the class itself, though it might include several supporting classes.

For those of you not familiar with TDD, suppose that you have a function that pulls a list of coordinates from the database and calculates the best fit line for those coordinates. Your unit test should not test the database call, as that is reaching across a boundary (from your class into the data access layer). Ideally, you should refactor the method so that another method performs the data access and provides the method you're testing with the coordinates it needs. This provides several key benefits:

  • Your function is no longer tightly coupled to the current system. You could easily move it to another system that happened to have a different data access layer.

  • Your unit test of this function no longer needs to access the database, helping to keep execution of your unit tests extremely fast.

  • Your unit test is prevented from being less fragile. Changes to the data access layer will not affect this function, and therefore, the unit test of this function.

Another example of a boundary is writing output, for example, writing content to the HTTP response. Chapter 5 discusses how action results generally handle framework level work. In part, this is to support this principle that unit tests should not cross boundaries. The code that you write in an action method can avoid crossing boundaries by encapsulating the boundary-crossing behavior within an action result. This allows your unit test to check that the action method set the values of action result correctly without actually executing the code that would cross the boundary.

Default Unit Tests

The default unit tests provided in the ASP.NET MVC Web Application project template demonstrates this principle. Let's take a quick look at those tests.

When you create a new project, the first screen you see is an option to select a unit test framework, as shown in Figure 10-1.

Image from book
Figure 10-1

Selecting the Visual Studio Unit Test option in the Test framework dropdown and then clicking OK creates a default unit test project with unit tests of the default action methods with the default HomeController class. Let's take a brief look at the Index method in HomeController and then we'll look at the unit test provided.

public ActionResult Index() {    ViewData["Title"] = "Home Page";    ViewData["Message"] = "Welcome to ASP.NET MVC!";    return View();}

This method is very simple. It adds a couple of strings to the ViewData dictionary and then returns a ViewResult instance via the View method. Now let's look at the unit test for this method.

public void Index() {    // Arrange    HomeController controller = new HomeController();    // Act    ViewResult result = controller.Index() as ViewResult;    // Assert    ViewDataDictionary viewData = result.ViewData;    Assert.AreEqual("Home Page", viewData["Title"]);    Assert.AreEqual("Welcome to ASP.NET MVC!", viewData["Message"]);}

Notice in the commented "Assert" section of the test, the unit test checks to see that the values specified within the ViewData is what you expected. More importantly, notice what the test doesn't check — that the actual view was rendered correctly as HTML and sent over the wire to the browser. That would violate the principle of not crossing boundaries. The test focuses on testing the code that the developer wrote.

Important 

Product Team Aside

Arranging, Act, Assert

You'll notice that in the comments of this unit test, the authors have grouped the code into three sections, using comments. This follows the Arrange, Act, Assert pattern first described by William C. Wake (http://weblogs.java.net/blog/wwake/archive/2003/12).

This has become a de facto standard pattern in organizing the code within a unit test. Keeping to this pattern helps those who read your tests quickly understand your test. This keeps you from having assertions all over the place within the test, making them hard to understand.

The assumption the developer makes here is that the ViewResult itself has its own unit tests, appropriate to that boundary. This is not to say that the developer should never test the full rendering of the action method. There is definitely a value to loading up the browser, navigating to the action, and making sure that all the individual components work together correctly. What you want to remember is that this test is not considered a unit test.

Be sure to look at the other unit tests included in the default template. They provide good examples of how to unit test controller actions. Most of the tests follow the same pattern:

  1. Instantiate the controller.

  2. Call an action method, casting the result to the expected action result type.

  3. Verify the action result has the expected values.

Controllers are where most of the action takes place in an ASP.NET MVC application, so it makes good sense to spend the bulk of your time writing unit tests of your controller code. The end of Chapter 5 digs into how to unit test controllers just a bit.

If you recall, the focus is not crossing boundaries with your unit tests. So rather than writing to the response and trying to get the output, you instead return an action result from your action method and within your unit test and confirm that the action result has the values that you expect. At this point in the unit test, you don't need to actually execute the action result as part of the unit test — you can save that for a functional test either by firing up the browser, or by automating the browser using a free tool like WatiN.

This may seem odd to those new to unit testing, but it follows another principle that is closely related to the principle of not crossing boundaries.

Only Test the Code That You Write

One mistake that many developers make when writing unit tests is testing too much in a single unit test. Ideally, your unit tests test the code that you're writing at the moment. The tests should not get diverted into testing other code, such as the code on which your code is dependent.

Let's look at a concrete example using the default About action method, which is part of HomeController in the default project template:

public ActionResult About(){    ViewData["Title"] = "About Page";    return View();}

Notice that in the last line, when the view is returned, it doesn't specify the view name. By convention, when no view name is specified, the name of the action method is used. Under the hood, the way that convention is enforced is that when ASP.NET MVC calls the ExecuteResult method on the action result returned by the action method, the current action method name is used if the current view name is not set.

Let's look at one approach a developer might try to take when testing this action:

[TestMethod]public void AboutReturnsAboutView(){    HomeController controller = new HomeController();    ViewResult result = controller.About() as ViewResult;    Assert.AreEqual("About", result.ViewName);}

A lot of developers who write such a unit test are surprised to find that it fails. But when you consider the principle of testing the code that you wrote, the failing result makes sense. Nowhere in the About method, which is the code that you wrote, did you set the ViewName to anything. In fact, the actual test you should be writing should be something along the lines of:

[TestMethod]public void AboutReturnsAboutView(){    HomeController controller = new HomeController();    ViewResult result = controller.About() as ViewResult;    //I explicitly want to rely on the framework to set the viewname    Assert.AreEqual(string.Empty, result.ViewName);}

By not setting the ViewName, you are indicating that you want the framework's behavior of applying the convention to take place. You don't need to test that the framework does the right thing, because you didn't write that code. The framework developers hopefully have good test coverage that the convention will be applied (they do!).

For the sake of argument, suppose that you did write the ViewResult class — what should you do then? You still shouldn't test it in this test. This test is focused on testing the About action method; thus, it should assume that the ViewResult class has the correct behavior. Instead, you would have another test that tests the ViewResult class. In this way, your tests are not highly coupled to each other, making them less fragile to change. If you change the behavior of ViewResult, you have a lot less tests to fix.

Of course, this doesn't mean you shouldn't quickly run the code and make sure that what you think the framework will do is exactly what it does. Unit tests do not negate the need for functional tests and integration tests. The key point here is that the unit test is focused on testing a unit of code that you wrote; testing how your code interacts with the actual framework should be left for a different type of testing.

What Are Some Benefits of Writing Tests?

Writing unit tests in this manner provides several benefits. The following table contains a short list of benefits of writing unit tests (it doesn't encompass all the benefits).

Open table as spreadsheet

Benefit

Description

Tests are unambiguous specifications.

As mentioned earlier in this chapter, the unit tests describe and verify the behavior of the code.

Tests are documentation.

Written documentation of code always grows stale, and it's difficult to verify whether or not documentation is up to date. Unit tests document the low-level behavior of code, and it is easy to verify that they are not stale; simply run them and if they pass, they are still up to date.

Tests are safety nets.

No safety net can guarantee 100 percent accuracy, but good unit test coverage can provide a level confidence that you're not breaking anything when making changes.

Tests improve quality.

Unit tests help improve the quality of design as well as the overall quality of your code.

How Do I Get Started?

As this is not a book about TDD, the authors must apologize for the very sparse treatment of TDD. As we stated in the introduction to this chapter, if you're not already practicing TDD, we're not trying to convert you, although we happen to be fans of this design process. However, if you found this intriguing and are interested in learning more, there are a couple of books we recommend taking a look at.

  • Test Driven Development: By Example by Kent Beck. Kent Beck is one of the originators of the modern form of Test Driven Development. The book walks through two different Java projects from beginning to end employing the practices of TDD.

  • Test-Driven Development in Microsoft .NET by James W. Newkirk and Alexei A. Vorntsov. This book might be more accessible to those who would prefer a .NET-focused book on TDD. James Newkirk was one of the original developers of NUnit, a popular unit testing framework for .NET.

 

Applying TDD to ASP.NET MVC

This section discusses ways to apply the practice of TDD to ASP.NET MVC. When we say this, the authors are really answering the question "How do I write unit tests for my ASP.NET MVC application?" Let's look at some examples.

Testing Routes

Routing is an important and essential feature leveraged by ASP.NET MVC. At first glance, routes look a lot like configuration information rather than code. When viewed that way, it leads us to wonder, "If TDD is a design activity for code, why would you apply it to configuration?" However, when you consider the fact that routes map requests to a type (IRouteHandler) that is used to invoke methods on controller classes, you start to realize that it is a good idea to treat routes as code. Poor design of your routes can leave whole sections of your application unreachable by accident.

The general pattern to testing routes is to add all your routes to a local instance of RouteCollection and then fake an HTTP request and confirm that the route data you expected to be parsed from the request was parsed correctly.

To fake a request, one approach you could take is to write your own test class that inherits from HttpContextBase and another that inherits from HttpRequestBase. Unfortunately, these classes have a lot of members, so this is a cumbersome task. This is why, for the purposes of illustration, you'll use a mock framework named MoQ (pronounced "mock-you"), which allows you to dynamically fake a class.

MoQ can be downloaded from http://www.mockframeworks.com/moq. You'll need to reference moq.dll in order to follow along.

Let's start off by demonstrating how to write a test of the "Default" route included in Global.asax.cs. This demonstration assumes that when you create a new project using the ASP.NET MVC Web Application template, you select an MSTest project.

public static void RegisterRoutes(RouteCollection routes){    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");    routes.MapRoute(        "Default",        "{controller}/{action}/{id}",        new { controller = "Home", action = "Index", id = "" }    );}

There are many tests you can write for this route, but let's start off with a simple one. When you make a request for /product/list, you expect that route data for "controller" will have the value "product" and the route data for "action" will have the value "list." Because you did not supply a value for "id" in your URL, you expect that the route data value for "id" will use the default value of an empty string.

Here's the code for the test:

using System.Web;using Moq;using System.Web.Routing;[TestMethod]public void CanMapNormalControllerActionRoute(){    //arrange    RouteCollection routes = new RouteCollection();    MvcApplication.RegisterRoutes(routes);    var httpContextMock = new Mock<HttpContextBase>();    httpContextMock.Expect(c => c.Request    .AppRelativeCurrentExecutionFilePath).Returns("~/product/list");    //act    RouteData routeData = routes.GetRouteData(httpContextMock.Object);    //assert    Assert.IsNotNull(routeData, "Should have found the route");    Assert.AreEqual("product", routeData.Values["Controller"]);    Assert.AreEqual("list", routeData.Values["action"]);    Assert.AreEqual("", routeData.Values["id"]);}

Let's dissect this code into small pieces:

  • The first two lines within the test method create a RouteCollection instance and then populate that instance using the RegisterRoutes method of the global application class (aptly named MvcApplication by default):

    RouteCollection routes = new RouteCollection();GlobalApplication.RegisterRoutes(routes);

    The whole reason for adding the RegisterRoutes method to the default template is to provide guidance for writing unit tests of routes. It is possible to unit test against the static singleton RouteTable.Routes collection, but this is not a good practice because unit tests should be run in isolation and should not share data with one another. It's very easy for a unit test to forget to clear that static collection. Ideally, unit tests should be able to run in parallel without any conflicts, hence the recommendation that unit tests populate a local instance of RouteCollection rather than going to RouteTable.Routes.

  • Notice that the GetRouteData method of RouteCollection requires that you pass an instance of HttpContextBase, which represents the current HttpContext. You don't want to pass in the real HTTP context instance because it is intimately tied to the ASP.NET pipeline. Instead, what you'd really like to do is supply a fake context that we have complete control over. And by complete control, we mean that when any method of that fake is called, you can tell the fake what to return in response.

  • The next four lines make use of the MoQ library to create the fake HTTP context we just mentioned:

    var httpContextMock = new Mock<HttpContextBase>();httpContextMock.Expect(c => c.Request   .AppRelativeCurrentExecutionFilePath)   .Returns("~/product/list");
    • The first two lines here create mocks of the HttpContextBase and HttpRequestBase abstract base classes.

    • The third line is an interesting one. Here, you are telling the HttpContextBase mock instance that when anyone asks it for a request object (via the Request property), to give it a fake request instead.

    • In the fourth line, you then tell the HttpRequestBase (our fake request) to do the same kind of substitution. When someone asks it for AppRelativeCurrentExecutionFilePath (essentially the URL from ASP.NET's point of view), use ~/product/list instead.

  • In the last five lines, you finally call the method you're actually testing, which is GetRouteData.

    RouteData routeData = routes.GetRouteData(httpContextMock.Object);Assert.IsNotNull(routeData, "Should have found the route");Assert.AreEqual("product", routeData.Values["Controller"]);Assert.AreEqual("list", routeData.Values["action"]);Assert.AreEqual("", routeData.Values["id"]);

    You then make three assertions to ensure that the route data is populated in the manner expected:

    • That the "controller" value is "product."

    • That the "action" is "list."

    • That the "id" is an empty string. You can take this approach to ensuring that your routes match the requests that you would expect them to.

Testing Controllers

Because controllers embody the bulk of your application logic, it makes sense to focus testing efforts there. The earlier section "Only Test the Code That You Write" jumped the gun a bit and covered the basics of testing controller action, which return a view result. This section goes into more details on testing controller actions that return different types of action results.

Important 

Product Team Aside

Testing the Model

Obviously, your business logic (aka model objects) contain plenty of important logic and are every bit as much deserving of testing as controllers. But testing business objects is not specific to MVC — one nice benefit of the Separation of Concerns. On a real project, these might be preexisting objects in a separate class library that you reuse from project to project. Either way, normal unit testing practices would apply in that case and, we assume for the purpose of this illustration, that you already have well tested model objects.

Redirecting to Another Action

There are other things your action may need to do that have dependencies on the underlying framework. One common action is to redirect to another action. In most cases, underlying framework actions are encapsulated by an ActionResult type, although not all cases. Let's look at an example of an action method that performs a redirect:

public ActionResult Save(string value){    TempData["TheValue"] = value;    //Pretend to save the value successfully.    return RedirectToAction("Display");}

Now this is a very contrived example, but by trying to incorporate a real-world example in a book, we will end up with more dead trees than necessary. This will do to illustrate the point here: the action method does something very simple; it stores a value in the TempData dictionary and then redirects to another action named "Display." At this point, you haven't implemented Display, but that doesn't matter. You're interested in testing the Save method right now, not Display.

Again, you can test this method by simply examining the values of the action result type returned by the action:

[TestMethod]public void SaveStoresTempDataValueAndRedirectsToFoo(){    var controller = new HomeController();    var result = controller.Save("is 42") as RedirectToRouteResult;    Assert.IsNotNull(result, "Expected the result to be a redirect");    Assert.AreEqual("is 42", controller.TempData["TheValue"];    Assert.AreEqual("Display", result.Values["action"]);}

One thing the authors have glossed over thus far is that you typically have the return type of the action method as ActionResult. The reason for this is that a single action method might return more than one type of result, depending on the path taken through the method. Normally, this isn't a problem because ASP.NET MVC is the one calling your action method, not another developer. But in the case of a unit test, it's convenient to cast that result to the expected type, to make sure that your method is behaving properly as well as to make it easier for you to examine its values. Notice that in the second line of this test, you cast the result to the type RedirectToRouteResult. Both the methods RedirectToAction and RedirectToRoute return an instance of this type.

After asserting that the type of the action method is what you expected, you assert two more facts:

  • Check that the value stored in the TempData dictionary is the one you expected — in this case, "42."

  • Make sure that the action you are redirecting to is the one you specified — in this case, "Display."

Important 

Product Team Aside

Testing with the TempDataDictionary

At runtime, the TempData dictionary by default stores its values in the Session. However, within a unit test project, it acts just like a normal dictionary. This makes writing tests against it easy and allows us to not cross boundaries here.

Testing View Helpers

View helpers are simply helper methods that encapsulate a reusable bit of view. Typically, these methods are implemented as extension methods on the HtmlHelper class. Let's look at a simple case of unit testing a helper method.

In this demonstration, say you've been tasked with writing a helper method that will generate an unordered list given an enumeration of elements. Let's start with the shell implementation of the method:

using System;using System.Collections.Generic;using System.Web.Mvc;public static class MyHelpers{    public static string UnorderedList<T>(this HtmlHelper html,      IEnumerable<T> items)    {        throw new NotImplementedException();    }}

Typically, you start off writing unit tests for the argument exception cases. For example, in this case, you would never expect the html argument to be null, so you should probably write a unit test for that:

[TestMethod]public void UnorderedListWithNullHtmlThrowsArgumentException(){    try    {        MyHelpers.UnorderedList(null, new int[] { });    }    catch (ArgumentNullException)    {        return;    }    Assert.Fail();}

In this case, the test will fail unless UnorderedList throws an ArgumentNullException. Sure enough, if you run this test, it fails because you haven't implemented UnorderedList, and it is still throwing a NotImplementedException.

Let's make this test pass and move on to the next.

public static string UnorderedList<T>(this HtmlHelper html, IEnumerable<T> items){    if(html == null)    {        throw new ArgumentNullException("html");    }    throw new NotImplementedException();}
Important 

Product Team Aside

On Not Using the ExpectedException Attribute

Some developers may find it odd that we're not using the [ExpectedException] attribute on the unit test here. In general, we try to avoid using that attribute because it is too coarse-grained for our needs. For example, in a multi-line test, it's impossible to know if the proper line of code is the one that threw the exception.

In this particular example, it's not really an issue, as there's only one line of code. The ASP.NET MVC team, for example, implemented its own ExceptionHelper .AssertThrows() method. which takes in and invokes an Action (the delegate of type Action, not to be confused with an MVC action) Some unit test frameworks, such as xUnit.net, include such a method directly.

General usage of the method is to pass in a lambda that calls the method or property you are testing. The AssertThrows method will assert that calling the action throws an exception.

Here's a pseudocode rewrite of the above test using this approach:

[TestMethod]public void UnorderedListWithNullThrowsArgumentException(){    Assert.Throws<ArgumentNullException>(() =>        MyHelpers.UnorderedList(null, new int[] { })    );}

You should probably do the same thing for items, but let's skip ahead to the meat of the implementation and write a test for the key purpose of this method. When you pass it an IEnumerable<T>, it should generate an unordered list. To write a test with what you expect to happen when you pass an array of integers (which happens to implement IEnumerable<int>), you'd do the following:

[TestMethod]public void UnorderedListWithIntArrayRendersUnorderedListWithNumbers(){    var contextMock = new Mock<HttpContextBase>();    var controllerMock = new Mock<IController>();    var cc = new ControllerContext(contextMock.Object, new RouteData(),      controllerMock.Object);    var viewContext = new ViewContext(cc, "n/a", "n/a", new ViewDataDictionary(),      new TempDataDictionary());    var vdcMock = new Mock<IViewDataContainer>();    var helper = new HtmlHelper(viewContext, vdcMock.Object);    string output = helper.UnorderedList(new int[] {0, 1, 2 });    Assert.AreEqual("<ul><li>0</li><li>1</li><li>2</li></ul>", output);}

There's a lot going on here:

  • The first six lines of code are necessary to create an HtmlHelper instance. You once again turn to MoQ to help instantiate fakes for several context classes you need in order to create the HtmlHelper instance.

  • Once you have that, you simply call the method and compare it to the expected output. Because your output is XHTML, you should really use an XML library to compare the output so that the test is less fragile to things like ignorable spacing issues.

In this case, when given an array of three numbers, you should expect to see those numbers rendered as an unordered list. Now you must implement your helper method:

public static string UnorderedList<T>(this HtmlHelper html, IEnumerable<T> items) {    if (html == null)    {        throw new ArgumentNullException("html");    }    string ul = "<ul>";    foreach (var item in items)    {        ul += "<li>" + html.Encode(item.ToString()) + "</li>";    }    return ul + "</ul>";}
Important 

Product Team Aside

A Note About String Concatenation

Some of you read that code and are thinking to yourself, "String concatenation!? Are you kidding?" Yeah, we could use a StringBuilder, but for a small number of concatenations, concatenating a string is faster than instantiating and using a StringBuilder. This just goes to show that before jumping to performance conclusions, measure, measure, measure. If you end up using this with a large number of items, you would probably want to change the implementation.

Now when you run your test, it passes.

Testing Views

If you look around the Web, you'll find that there isn't much content out there on unit testing views. For the TDD practitioner, the view should only contain presentation layout code and no business code. Since TDD is a code design activity, writing unit tests wouldn't really apply in this situation.

For those who don't practice TDD, but do write unit tests, there's a purely practical consideration for not unit testing views. Views tend to be the most volatile area of an application in terms of change. If you've ever worked with a pixel-perfect stakeholder, constantly asking to move elements around the UI, you understand what we mean. Attempting to keep unit tests up to date in such an environment would drive a Zen master insane with frustration.

It is possible to write unit tests that supply some data to a view and examine the rendered markup, but that really isn't testing the view properly. If you're really interested in testing the view, you need to interact with the view. At that point, you've crossed over into QA territory into the realm of functional testing. In this case, having a human being try out the app is extremely important. In some cases, functional test automation is possible and useful, but with a tool specifically designed for this sort of testing. For example, a great tool for automating such system tests is WATIN, which can automate a browser interaction with a web site.

 

Summary

With all this discussion on TDD, you might have the impression that TDD on ASP.NET is only possible with ASP.NET MVC. Model-View-Controller is just one form of a larger collection of patterns called "Separated Presentation" (see http://martinfowler.com/eaaDev/SeparatedPresentation.html). If you're using ASP.NET Web Forms, for example, you might choose Model-View-Presenter (MVP for short) to test your code. The Patterns and Practices group at Microsoft, for example, provide a download called the Web Client Software Factory (WCSF) that embodies the MVP pattern and provides guidance for unit testing on Web Forms.

However, when ASP.NET was first written, TDD wasn't really on the radar as much as it is today. So there are many situations that make applying TDD a little rough around the edges with Web Forms. MVC is being designed with first class support for TDD in mind. This is why you hear about TDD with ASP.NET MVC. It doesn't mean that you have to use TDD with this framework if you don't want to — that's really up to you. However, if you do, we hope you find it to be a smooth and happy experience. Just ask Rob about it.

posted on 2010-06-26 13:34  Yan-Feng  阅读(410)  评论(0编辑  收藏  举报