Recently, I had a disagreement with a colleague over the correct way to do Test-Driven Development. This is an important disagreement because it affects the design of the ASP.NET MVC framework.
According to my co-worker (let’s call him Tad), there is no difference between Test-First Development and Test-After Development “except when you write your unit tests.” Tad is a practitioner and proponent of Test-After Development. When you practice Test-After Development, you write application code first and then you write a unit test that tests the application code.
From the perspective of someone who practices Test-Driven Development, this gets things backwards. I believe that it is an essential part of Test-Driven Development that you must write your unit test before writing any application code. Why does it matter?
Test-Driven Development is first and foremost an application design methodology. If you write your unit tests after you write your application code, then you are not driving the design of your application with your unit tests. In other words, Test-After Development ignores the Driven in Test-Driven Development.
In order to support Test-Driven Development, the ASP.NET MVC framework needs to support two things: testability and incremental design (what Martin Fowler calls Evolutionary Design). If you are only interested in Test-After Development, then you will ignore this second requirement to the detriment of those of us who are interested in true Test-Driven Development.
Let’s consider a concrete scenario: building a forums application.
Building a Forums Application with Test-Driven Development
Here are the steps that I would follow to build a forums application by using Test-Driven Development:
1. Write a list of user stories that describe what the forums application should do. These user stories should be non-technical (the type of thing that a customer would write).
2. Pick a user story and express the user story in a unit test.
3. Write just enough code to pass the unit test. In other words, do the simplest thing that could possibly work to pass the unit test.
4. Consider refactoring my code to improve the design of my application. I can fearlessly refactor because my code is covered by unit tests (see RefactorMercilessly).
5. Repeat steps 2 – 3 until I have completed the application (keeping in mind that the user stories might change over the course of the process of writing the application).
So, I might start with a list of user stories that look like this:
1. Can see all of the forum posts
2. Can create a new forum post
3. Can reply to a forum post
And, I would express the requirement embodied in the first user story with a unit test that looks like this:
[TestMethod]
public void CanListForumPosts()
{
// Arrange
var controller = new ForumController();
// Act
var result = (ViewResult)controller.Index();
// Assert
var forumPosts = (ICollection)result.ViewData.Model;
CollectionAssert.AllItemsAreInstancesOfType(forumPosts, typeof(ForumPost));
}
This unit test verifies that invoking the Index() action on the Forum controller class returns a collection of forum posts. Currently, this unit test fails (I can’t even compile it) because I have not created a ForumController or ForumPost class.
Following good Test-Driven Development design methodology, at this point, I am only allowed to write enough code to make this unit test pass. And, I should make the test pass in the easiest and simplest way possible (I’m not allowed to go off and write a massive forums library however tempting that might be).
To make this test pass, I need to create a ForumsController class and a ForumPost class. Here’s the code for the ForumsController class:
using System.Collections.Generic;
using System.Web.Mvc;
using Forums.Models;
namespace Forums.Controllers
{
public class ForumController : Controller
{
//
// GET: /Forum/
public ActionResult Index()
{
var forumPosts = new List<ForumPost>();
return View(forumPosts);
}
}
}
Notice how simple the Index() method is. The Index() method simply creates a collection of forum posts and returns it.
From the perspective of good software design, this controller is horrible. I’m mixing responsibilities. My Data access code should go in a separate class. And, even worse, this controller doesn’t actually do anything useful at the moment.
However, from the perspective of Test-Driven Development, this is exactly the right way to initially create the Forums controller. Test-Driven Development enforces incremental design. I am only allowed to write enough code to pass my unit tests.
Test-Driven Development forces developers to focus on writing the code that they need right now instead of writing code that they might need in the future. Two of the important guiding principles behind Test-Driven Development are "Keep It Simple, Stupid" (KISS) and "You Ain't Gonna Need It" (YAGNI) (see Wikipedia and C2).
Eventually, after repeating the cycle of writing a unit test and writing just enough code to pass the test, you will start to notice duplication in your code. At that point, you can refactor your code to improve the design of your code. You will be able to refactor your code fearlessly because your code is covered by unit tests.
The important point here is that the design of your application should be driven by your unit tests. You don’t start with design principles and create an application. Instead, you incrementally improve the design of your application after each cycle of test and code.
Building a Forums Application with Test-After Development
A proponent of Test-After Development takes a very different approach to the process of building an application. Someone who practices Test-After Development starts by writing application code and then writes a unit test after the application code is written. More to the point, a proponent of Test-After Development makes all of their design decisions up front.
The crucial difference between Test-Driven Development and Test-After Development is a difference in belief about the importance of incremental design. Practitioners of Test-Driven Development take baby steps in improving the design of an application. Practitioners of Test-After Development attempt to implement good design from the very start.
Here are the steps that a practitioner of Test-After Development would take to building a forums application:
1. Create a list of user stories.
2. Consider the best design for the application (create separate controller and repository classes).
3. Write application code that follows the design.
4. Write unit tests for the code.
5. Repeat steps 2 – 4 until the forums application is completed.
Unlike someone who practices Test-Driven Development, a proponent of Test-After Development would start by creating separate Forums controller and repository classes.
For example, the Forums controller would look like this:
using System.Web.Mvc;
using TADApp.Models;
namespace TADApp.Controllers
{
public class ForumsController : Controller
{
private IForumsRepository _repository;
public ForumsController()
:this(new ForumsRepository()){}
public ForumsController(IForumsRepository repository)
{
_repository = repository;
}
public ActionResult Index()
{
var forumPosts = _repository.ListForumPosts();
return View(forumPosts);
}
}
}
And, the repository class would look like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace TADApp.Models
{
public interface IForumsRepository
{
IEnumerable<ForumPost> ListForumPosts();
}
public class ForumsRepository : IForumsRepository
{
private ForumsDBEntities _entities = new ForumsDBEntities();
#region IForumsRepository Members
public IEnumerable<ForumPost> ListForumPosts()
{
return _entities.ForumPostSet.ToList();
}
#endregion
}
}
Next, the proponent of Test-After Development would create a unit test for the Forums controller that looks like this:
[TestMethod]
public void CanListForumPosts()
{
// Arrange
var mockRepository = new Mock<IForumsRepository>();
mockRepository.Expect(r => r.ListForumPosts()).Returns(new List<ForumPost>());
var controller = new ForumsController(mockRepository.Object);
// Act
var result = (ViewResult)controller.Index();
// Assert
var forumPosts = (ICollection)result.ViewData.Model;
CollectionAssert.AllItemsAreInstancesOfType(forumPosts, typeof(ForumPost));
}
This unit test mocks the Forums repository (by mocking the IForumsRepository interface) and verifies that the Forums controller returns a set of forum posts.
Unit Tests versus TDD Tests
One place where the proponent of Test-Driven Development and the proponent of Test-After Development strongly differ is on the subject of unit tests. I disagree with Tad about the purpose -- and the correct way to write -- unit tests.
When I practice Test-Driven Development, I start with a test and then I write just enough code to pass the test. I use the tests as a safety net for change. In particular, I use the tests as a safety net so I can fearlessly refactor my application code to improve the design of my application.
When using Test-Driven Development to create the forums application, my first test verified that the Forums controller returns a list of forum posts. I would keep that test even after I refactor the design of my application to migrate my data access logic into a separate repository class. I need the original test to verify that I haven’t broken my original application code when refactoring my application to have a better design.
My unit tests flow directly from the user stories. After I add a unit test, I almost never remove it. I might refactor my unit tests to prevent code duplication in my tests. However, I don’t change what the unit tests are tests for.
A proponent of Test-After Development, in contrast, is constantly changing their tests. When Tad rewrites his application logic, Tad rewrites his unit tests. Tad’s unit tests are driven by his application design.
From the very beginning, Tad would create a separate Forums controller class and repository class. He would create distinct sets of unit tests for the Forums controller and the repository class. When Tad refactors his application to improve the design of his application, Tad rewrites his unit tests.
Suppose, for example, that both Tad and I decided to add support for validation to the Forums application. If someone submits a forum post with an empty Title, we both want to display a validation error message.
I would take the approach of testing whether or not the forums controller returns a validation error message in ModelState when I attempt to create an invalid forum post. My unit test would look something like this:
[TestMethod]
public void ForumPostSubjectIsRequired()
{
// Arrange
var controller = new ForumController();
var postToCreate = new ForumPost { Subject = string.Empty };
// Act
var result = (ViewResult)controller.Create(postToCreate);
// Assert
var subjectError = result.ViewData.ModelState["Subject"].Errors[0];
Assert.AreEqual("Subject is required!", subjectError.ErrorMessage);
}
This test verifies that a validation error message is included in model state when you attempt to create a new forum post without supplying a subject. Regardless of how I end up refactoring my application (for example, to use a separate validation service layer), I would keep this unit test to verify that my application continues to satisfy the requirement expressed by the user story.
Tad, on the other hand, would never create a test that verifies whether or not the Forums controller returns a validation error message. Tad would argue that it is not the responsibility of a controller to perform validation. The responsibility of a controller is to control application flow.
Tad would write a unit test for his validation logic. However, the nature of his unit tests would be dependent on the architectural design of his application. If Tad uses a separate service layer to contain his validation logic, then he would write unit tests that verify the behavior of the service layer. If Tad uses validator attributes to perform validation, then he would write unit tests that verify the presence of the expected validator attributes.
Tad would argue that my unit tests aren’t really unit tests at all. Over time, as the design of my application evolves, my unit tests start to resemble functional (or acceptance) tests. They are really verifying the outputs of the application given a certain input. My unit tests are independent of the application design.
I would agree with Tad, but I would argue that the tests that you write when performing Test-Driven Development have a different purpose than standard unit tests. A TDD test, in contrast to a unit test, does not necessarily test a separate unit of code. Instead, a TDD test is used to test “little areas of responsibility, and that could be a part of a class or it could be several classes together” (see Martin Fowler).
This is not to say that a TDD test is the same as an acceptance test. An acceptance test is used to test an application end-to-end (with the database and UI hooked up). A TDD test, on the other hand, is not an end-to-end test. A TDD test does not have external dependencies and it is designed to be executed very fast. A TDD test is used to test whether a particular requirement derived from a user story has been satisfied (see Uncle Bob).
From the perspective of Test-Driven Development, the purpose of unit tests is to drive the design of an application. A unit test tells me what application code I need to write next. For example, I don’t know how I will implement my validation logic when I create a unit test. I should not be making these design decisions up front. My test tells me what I am allowed to do and what I must do. The unit test provides me with the minimum and maximum criterion for success.
My primary objection to Tad’s approach to building applications is that it forces premature design decisions. Tad makes design decisions first and then creates his unit tests. I create my unit tests first and then create my design (see Jeff Langr). Tad’s approach does not allow for an Evolutionary Approach to design.
Conclusion
So why should any of this matter? The ASP.NET MVC framework was designed to be highly testable. Therefore, it should keep proponents of both Test-Driven Development and Test-After Development happy. Right?
The point of this blog entry is to claim that the ASP.NET MVC framework needs to support more than testability to support Test-Driven Development. To enable Test-Driven Development, the ASP.NET MVC framework was designed to support both testability and incremental design. From the perspective of a practitioner of Test-Driven Development, if a framework does not enable you to get from point A to point B by taking baby design steps, then there is no way to get to a well designed application at all.