The MS Test unit-testing framework is included with all paid editions of Visual Studio 2010 (it is not included in Visual Web Developer Express 2010); if you're using Visual Studio 2012, unit testing is even included in the free editions and contains a much-improved unit test runner. Although you can create unit test projects directly inside of Visual Studio, it can be a lot of work getting started with unit testing your MVC application. The ASP.NET MVC team included unit-testing capability in the New Project dialog for MVC applications, as shown in Figure 13.1.
By selecting the Create a Unit Test Project checkbox, you're telling the ASP.NET MVC New Project Wizard not only to create an associated unit test project, but also to populate it with a set of default unit tests. These default unit tests can help new users understand how to write tests against an MVC application.
The default application templates give you just enough functionality to get you started with your first application. When you create the new project, it automatically opens HomeController.cs for you. HomeController.cs contains three action methods (Index, About, and Contact). This is the source for the Index action:
public ActionResult Index() { ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application."; return View(); }
This is fairly straightforward code. A welcome message is set into the weakly typed data sent to the view (the ViewBag object), and then a view result is returned. If you expected the unit tests to be relatively simple, you'd be right. In the default unit test project, there is exactly one test for the Index action:
[TestMethod] public void Index() { // Arrange HomeController controller = new HomeController(); // Act ViewResult result = controller.Index() as ViewResult; // Assert Assert.AreEqual("Modify this template to jump-start your ASP.NET MVC application.", result.ViewBag.Message); }
This is a pretty good unit test: it's written in 3A form, and at three lines of code, it's quite simple to understand. However, even this unit test has room for improvement. Our action method is only two lines of code, but it's actually doing three things:
For starters, you can see that this unit test is actually testing two of these three concerns (and it has a potential subtle bug, at that). Since you want your unit tests to be as small and single-focused as possible, you can see that you probably have at least two tests here (one for the message and one for the view result); if you wanted to write three, we wouldn't fault you for it.
The subtle bug in the test is the use of the as keyword. The as keyword in C# attempts to convert the value to the given type, and if it's not compatible, it returns null. However, in the assertion, the unit test dereferences the result reference without ever checking to see if it's null. Let's mark that up as a fourth concern to be tested: the action method should never return null.
The cast is an interesting code smell — that is, something you look at and wonder whether it's really the right thing. Is the cast really necessary? Obviously, the unit test needs to have an instance of the ViewResult class so that it can get access to the ViewBag property; that part isn't in question. But can you make a small change to the action code so that the cast is unnecessary? You can, and should:
public ViewResult Index() { ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application."; return View(); }
By changing the return value of the action method from the general ActionResult to the specific ViewResult, you've more clearly expressed the intention of your code: This action method always returns a view. Now you're down from four things to test to three with just a simple change of the production code. If you ever need to return anything else besides ViewResult from this action (for example, sometimes you'll return a view and sometimes you'll do a redirect), then you're forced to move back to the ActionResult return type. If you do that, it's very obvious that you must test the actual return type as well, because it won't always be the same return type.
Go ahead and rewrite the one test into two:
[TestMethod] public void IndexShouldAskForDefaultView() { HomeController controller = new HomeController(); ViewResult result = controller.Index(); Assert.IsNotNull(result); Assert.IsNull(result.ViewName); } [TestMethod] public void IndexShouldSetWelcomeMessageInViewBag() { HomeController controller = new HomeController(); ViewResult result = controller.Index(); Assert.AreEqual("Modify this template to jump-start your ASP.NET MVC application.", result.ViewBag.Message); }
You should feel much better about these tests now. They're still simple, but they should be free of the subtle bugs that affected the other test, and you're clearly testing the two pieces of independent behavior that are happening in this action method. It's also worth noting that you've given the tests much longer and more descriptive names. I've found that longer names mean you're more likely to understand the reason a test fails without even needing to look at the code inside the test. You might have no idea why a test named Index might fail, but you have a pretty good idea why a test named IndexShouldSetWelcomeMessageInViewBag would fail.
[TestClass] public class IndexTests { private HomeController controller; private ViewResult result; [TestInitialize] public void SetupContext() { controller = new HomeController(); result = controller.Index(); } [TestMethod] public void ShouldAskForDefaultView() { Assert.IsNotNull(result); Assert.IsNull(result.ViewName); } [TestMethod] public void ShouldSetWelcomeMessageInViewBag() { Assert.AreEqual(”Modify this template to jump-start your ASP.NET MVC application.”, result.ViewBag.Message); } }
One of the more common mistakes that people new to unit testing and TDD make is to test code they didn't write, even if inadvertently. Your tests should be focused on the code that you wrote, and not the code or logic that it depends upon.
For a concrete example, look at this example action method:
public ActionResult About() { return View(); }
Action methods don't get much simpler than this. You should be able to get away with a fairly simple unit test for this code:
[TestMethod] public void AboutShouldAskForDefaultView() { HomeController controller = new HomeController(); ViewResult result = (ViewResult)controller.About(); Assert.IsNotNull(result); Assert.IsNull(result.ViewName); }
When a controller action is invoked and a view is rendered by the MVC pipeline, a whole lot of stuff happens: Action methods are located by MVC, they are called with model binders invoked for any action parameters, the result is taken from the method and executed, and the resulting output is sent back to the browser. In addition, because you asked for the default view, that means the system attempts to find a view named About (to match your action name), and it will look in the ∼/Views/Home and ∼/Views/Shared folders to find it.
This unit test doesn't concern itself with any of that code. We focus on the code under test and none of its collaborators. Tests that test more than one thing at a time are called integration tests. If you look, there are no tests anywhere for that because all the rest of that behavior is provided by the MVC framework itself, and not any code you wrote. From a unit test perspective, you must trust that the MVC framework is capable of doing all those things. Testing everything running together is also a valuable exercise, but it's outside the scope of unit testing.
Let's focus for a moment on the ViewResult class. That is a direct result of calling the About action. Shouldn't you at least test its ability to look for the About view by default? You can say no, because it is code you didn't write (the MVC framework provided it), but even that argument isn't necessary. You can say no, even if it was your own custom action result class, because that's not the code you're testing right now. You are currently focused on the About action. The fact that it uses a specific action result type is all you need to know; exactly what it does is the concern of the unit test for that piece of code. You can safely assume, whether the action result is written by you or by the ASP.NET team, that the action result code is sufficiently tested on its own.