A couple weeks ago we looked at the importance of making tests robust - which can be accomplished by keeping them simple, focused and loosely coupled. However, as you test more complicated methods, you'll find that no matter how well-written your tests are, they'll bloat and grow brittle. Let's look at a simple example:
public ActionResult Save(int userId)
{
User user = repository.LoadUser(userId);
//todo handle null user
user.Name = Request.Form["name"];
user.Password = Request.Form["password"];
try
{
user.Update();
}
catch (ValidationException ve)
{
ViewData["Errors"] = ve;
}
return View();
}
Here we have a controller action responsible for handling changes made to a user. Pretty straightforward stuff, right? Unfortunately, the code isn't particularly easy to unit test (well, it isn't really difficult, but we're using a straightforward example for demonstration purposes). The problem is that our action actually has a number of distinct behaviours. One solution is to write a single monolithic test to cover everything in our method. Of course, we already know that'll make it unbelievably brittle, as well as making hard to test variations (different inputs, different return values). The other solution is to make extensive use of stubs and make sure each dependent call is accounted for.
For example, if we want to write a test that focuses on the handling of ValidationException, we need to do quite a bit of stubbing:
[Test]
public void Save_SetsValidationErrorsInViewData()
{
var controller = Mock.PartialMock<UsersController>();
var repository = Mock.StrictMock<IRepository>();
var user = Mock.PartialMock<User>();
var expected = new ValidationException("name", "invalid");
Mock.SetFakeControllerContext(controller);
repository.Stub(r => r.LoadUser(3)).Return(user);
controller.Request.Stub(r => r.Form)
.Repeat.Any()
.Return(new NameValueCollection());
user.Stub(u => u.Update()).Throw(expected);
controller.Save(3);
Assert.AreSame(expected, controller.ViewData["Errors"]);
}
Even for this simple example, the test is less than ideal. We need to setup a fake controller context (using the MvcMockHelper), handle our repository interaction and our Request.Form mapping. Worse, the same setup is required for each behaviour of this method that we'll test. The solution is to break-down our method into smaller more focused chunks. Here's an extreme example:
public ActionResult Save(int userId)
{
User user = BindUser(userId, Request.Form);
UpdateUser(user);
return View();
}
internal virtual User BindUser(int userId, NameValueCollection parameters)
{
User user = repository.LoadUser(userId);
user.Name = parameters["name"];
user.Password = parameters["password"];
return user;
}
internal virtual void UpdateUser(User user)
{
try
{
user.Update();
}
catch (ValidationException ve)
{
ViewData["Errors"] = ve;
}
}
Now, take a look at our test:
[Test]
public void UpdateUser_SetsValidationErrorsInViewData()
{
var user = Mock.PartialMock<User>();
var controller = new AwardsController();
var expected = new ValidationException("name", "invalid");
user.Expect(u => u.Update()).Throw(expected);
controller.UpdateUser(user);
Assert.AreSame(expected, controller.ViewData["Errors"]);
}
The test is majorly simplified - in fact, we don't even have to worry about our controller's context, or our interaction with the repository. The best part is that each of our methods behaviours (get the user, bind it, save it and mange the viewdata+view) can now be tested with equally simple test - rather than all-encompassing ones.
While I realize refactoring into really small methods can pose a psychological barrier for many, it's a truly useful technique with benefits beyond testability.
Posted
10-15-2008 8:27 PM
by
karl