The Reality of Getting Started with Test-Driven Development
Moving to TDD with ASP.NET is not, in fact, as easy as everyone tells you it is. But it's not as much work as you might think, either. Here's what you need to do to start doing TDD in the real world with an existing ASP.NET MVC application.
When I start extolling the wonders of test-driven development (TDD) to ASP.NET MVC programmers who aren't using it, I usually get a lot of eye rolling. Those developers raise three concerns about going to TDD:
- "I'm sure it's great with new development but I spend 75 percent of my time modifying existing code that has no tests…and my code isn't set up to support TDD, anyway."
- "I've got applications to deliver and deadlines to meet. I don't have time to set up the environment and write all those tests."
- "I write business applications which are all about storing and retrieving data. How do you expect me to put together a test when the results will change whenever the database changes?"
And these are all valid objections. I'll even add one: If you're only making changes to your Views, TDD is irrelevant to you. On the other hand, if you're making a change to a Controller (or some other class) then you can do TDD. And you should.
The reason that you should is that TDD will reduce the time you spend writing your code (while also improving your code's reliability, of course). The only way to create code faster than by using TDD is not to do any testing at all. My goal in this column (and a later one) is to show you how to achieve that in the real world.
In fact, I don't think that the objections listed above are the real reason that an ASP.NET MVC developer won't have moved to TDD. I think that, for most developers, the biggest issue is that they have a working UI that they've been using for testing. That form of testing – as inefficient as it is compared to using the TDD environment – has become a habit and, as a result, easier to keep doing than to change.
Setting Up to TDD for an ASP.NET MVC Application
But if you're working on an existing application, you can create your first test against an Action method you're creating or modifying in less than 15 minutes – and every subsequent test will take just a minute or two to create.
The first test takes the longest because you have to create and configure a Unit Test project for your ASP.NET application. In theory, you just need to do four things:
Add a Test project to your solution, giving it a name like [YourASP.NETProject]Test
Use NuGet to add ASP.NET MVC to your Test project so that your code be able to work with the ASP.NET objects
Add to your Test project a reference to your ASP.NET project
4) Rename the default class in your Test project to something like [YourControllerName]Test
Altogether that will take about five minutes (you'll also need to add some Imports/using directives to the top of your test class but you can have Visual Studio generate those as you need them).
Unfortunately, you'll also find that, after those four tasks, there are some additional "ditz-and-fix" tasks that you have to perform before you can write your first test. If, for example, your ASP.NET MVC application references some additional Class Libraries that you've created then you'll also need to add, to your Test project, references to those Class Library projects. If your ASP.NET MVC application has any connection strings or user settings in its config file then you'll almost certainly need to copy those to your Test project's config file.
This isn't a lot of work (and you'll only have to do it the first time you set up your Test project) but it does mean that you've spent 15 minutes on your Test project and you haven't, yet, written any actual code.
Writing Your First Test
But now you're ready to write your first test. For an Action method that accepts some parameters and displays the correct data in View, your test is easy to write. You just:
Declare your variables
Instantiate the Controller
Call the Action method, passing the parameters you want to test with
Catch the result of the Action method
Convert it to a ViewResult
Check the ViewName property to see if your Action method specified the right View
Check the Model property to see if your Action loaded the right data
Check anything else that matters to you
Listing 1 shows the code (I've numbered the lines so I can refer to them later).
Listing 1: Typical Code To Check the Output of a Controller's Action Method
2. Public Sub GetSalesOrderTest()
3. cust = New Customer
4. cust.Id = "A123"
5. cust.Name = "Peter Vogel"
6. //set more customer properties
7. Dim vr As ViewResult
8. Dim cc As Controller
9. Dim cust As Customer
10. Dim so As SalesOrder
11. cc = New CustomerController
12. vr = CType(cc.DisplayLatestSalesOrder(cust), ViewResult)
13. Assert.AreEqual("DisplayLatestSalesOrder", vr.ViewName)
14. salesOrder = CType(vr.Model, SalesOrder)
15. Assert.AreEqual("A123", SalesOrder.Id)
17. End Sub
You can see that my test code falls into the three parts that are typical of most tests. First, I Arrange for the test (steps 1 to 11, above), then I Act to execute the code for the test (just step 12 in this case) and, finally, I Assert (steps 12 through 15) to check my results. In addition to checking the ViewState returned by my method, I also always check the ModelState property just to make sure that I'm not ignoring any errors that I care about.
Not all asserting requires using the Assert class as step 12, which does double duty as an Act and an Assert demonstrates. In that one step, I call my Action method and attempt to convert the result into a ViewResult object. If the conversion fails (because, for example, I've returned a JsonResult), my test will blow up. Since Visual Studio will consider that as my test failing, I have also checked that my test is, in fact, returning a ViewResult.
If you'd prefer a cleaner separation between Acts and Asserts, you can use a method on the Assert class to determine if the ActionResult returned by the Action method is a ViewResult with code like this:
cc = New CustomerController
Dim obj As Object
obj = cc.DisplayLatestSalesOrder(cust)
vr = CType(obj, ViewResult)
Now that your test project is set up, testing your next Action method consists of copying/pasting your first test and then tweaking it to match what that Action method returns -- a couple of minutes' work, at most.
But that just raises the question of why you'd spend the time setting up your test project at all.
The first benefit occurs when your code fails your test (and it probably will the first time). With TDD, you put a breakpoint in your test on the line where you instantiate the Controller, right click in the method, select Debug Tests, and step through your code until you find the problem. That's going to be a lot faster and easier than trying to do the same thing by starting up a browser and navigating to this method.
In fact, I'd suggest that whatever testing you're doing now is always going to be easier with TDD than by navigating through your code using a browser: right click, select Run Tests, and you have your result. Plus, with TDD, you can do something you've never had time for before: Regression testing. After you get your second test working, you click in your test class outside of any test method and select Run Tests. That will re-run all of your tests in that class and reassure you that your new code hasn't damaged any of your previous code.
Building Test Cases
In fact, the real pain in this test is in creating that dummy Customer object to pass to the Action method (especially if the Customer object has lots of properties). However, if you need this Customer object for one method, it's entirely possible that you'll need to use it in other test methods. I'd recommend cutting and pasting that code into a method of its own in your test class so you can call it from any test that needs a Customer object.
Refactoring my Test to do that, gives code like Listing 2.
Listing 2: Refactored Test To Create a Reusable Customer Object
Public Function TypicalCustomer() As Customer
cc = New CustomerController
cust = New Customer
cust.Id = "A123"
cust.Name = "Peter Vogel"
//set more customer properties
Public Sub GetSalesOrderTest()
cust = GetTypicalCustomer
If my experience is any guide, over time you'll build up a library of methods that return objects useful for testing. For one test you might, for example, need a Customer with a bad credit rating or, for another test, a Customer that has no shipping address. To create those Customer objects, you just copy/paste your TypicalCustomer method, give the method a new name, tweak the object's property settings, and use your new method in the tests that need it.
How Much Testing?
You will need to decide how much testing you want to do around the Model property. Is it enough for you if the property is not null? Do you need to check that a particular object is in the property? How many of the properties on that single object do you feel you need to test to make sure that your Action method has done the right thing? If the Action method is returning a collection, do you just need to check to see how many objects are in the Model property? Or do you need to make sure that you have all (and only) the right objects in that collection?
Some tests with the Model property are relatively easy. If my Model holds a collection of objects I can determine that all the items in the Model property meet a certain condition with the All extension method. This code, for example, would confirm that all of the retrieved SalesOrders have a date some time in the future:
Dim sos As List(Of SalesOrder)
sos = CType(vr.Model, List(of SalesOrders))
Assert.IsTrue(sos.All(Function(o) o.DueDate > DateTime.Now))
However, that's not quite the same as ensuring that you've retrieved all of the orders that are in the future. For that you need to need to know how many "future orders" exist in the database.
There are a couple of ways to handle that problem. In your test, you could run a LINQ/Entity Framework query to count the number of orders and compare it to the number of orders in the Model property; Alternatively, you could maintain a test database with a specific number of future orders so that you know that the right answer is always "6".
However, I think that the solution that requires the least work is to create a mock database in your code. That's sufficiently interesting that I'll discuss it in a later column, along with handling ASP.NET specific features like the Session object.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.