Sunday, January 23, 2011

So, TDD is hard? Start decoupling!

As a lot of other TDD people, I try to push TDD onto my colleagues. It seldom works, and I think that's a shame. Therefore, lately I've begun to think about why a lot of other people find TDD painful, unproductive, and plain irritating when my own experience is the direct opposite.

I'd like to start a discussion about doing TDD and other agile practices in the real world, with real-world code examples, and this is the first blog post in this series.

The thing I want to point out in this post is that most of the time, people holding a grudge against TDD don't practice enough decoupling. Let's take an example of some code (practically) from the real world that I stumbled upon the other day. I understand those who wouldn't like to test this.

public ModelAndView createUser(long applicationId, String comment) {
  Application application = applicationDao.get(applicationId);
  application(OrderState.GRANTED);
  if (comment != null && !comment.isEmpty()) {
    application.setProcessingComment(comment);
  }
  applicationDao.save(application);
  Address address = application.getContactAddress();
  SocialSecurityNumber socialSecurityNumber = order.getSocialSecurityNumber();
  if (userDao.findUser(socialSecurityNumber) == null) {
    userDao.createUser(socialSecurityNumber, address);
  }
  if (application.isAdministrator()) {
    HashSet roles = new HashSet<role>();
    roles.add(new AdministratorRole(timeService.getTime(), null));
    roleDao.addRoles(socialSecurityNumber, roles);
  }
  return new ModelAndView("users/create", "message", "User created");
}

So, how would we test this? Well, we need to create a mock for an ApplicationDao, UserDao, and RoleDao. And we need to mock these method calls:

applicationDao.get(...), returning an Application
applicationDao.save(...)
userDao.findUser(...), returning a User
userDao.createUser(...)
timeService.getTime(), returing a Date
roleDao.addRoles(...)

But even worse, our logic needs to know a lot about different objects:

  • How to change an application so it gets to the "granted" state.
  • How to make sure that a user exists in the database.
  • How to promote a user to administrator.
  • How to construct an AdministratorRole.

This directly affects the cyclomatic complexity, so we need a huge amount of test cases to cover all possible paths through the code.

I understand why people find TDD frustrating when writing this kind of code.

But let's refactor it just a little bit:

public ModelAndView createUser(long applicationId, String comment) {
  Application application = applicationDao.get(applicationId);
  applicationDao.grant(application, comment);
  Address address = application.getContactAddress();
  SocialSecurityNumber socialSecurityNumber = order.getSocialSecurityNumber();
  userDao.ensureUserExists(socialSecurityNumber, address);
  if (application.isAdministrator()) {
    roleDao.makeAdministrator(socialSecurityNumber);
  }
  return new ModelAndView("users/create", "message", "User created");
}

In the real world, we shouldn't stop here - the method could easily be split up further. However, for this blog post, we'll stop with the refactoring now.

Anyway, with that code, we'll need to mock these method calls:

applicationDao.get(...), returning an Application
applicationDao.grant(...)
userDao.ensureUserExists(...)
roleDao.makeAdministrator(...)

Small change? Well, the code no longer needs to know how to change the state of the application, how to make sure a user exists in the database, or how to promote a user to administrator. As a result, the cyclomatic complexity of the new code is much lower, so just two tests will cover the positive cases. Of course, we've introduced new methods in the other services which need to be tested in their respective test suites, but each of them should be pretty straight-forward to test.

And in fact, we've done only two things in the refactoring:

  • Tell, don't ask. Instead of fishing out values and passing them back and forth, we've defined new methods that just "do the job", without us having to figure out how to meddle with a lot of object in the same method.
  • Flesh out responsability. In our method, we don't want to know that in order to promote a user to administrator, we need to give the user a certain role. That's what RoleDao knows, and there's no need to spread that knowledge to all our classes.

These simple tools are great when you're trying to decouple your components, and incidentally, you end up with more testable code.

And even better, if you remember these tools when you're test-driving your code, you'll start off writing nice, decoupled code.

Don't be afraid of writing a lot of small classes with their own, very limited responsability. Each of them will be easy to test, and your code base will be more readable.

1 comment:

  1. I agree. The difference in structure between TDD and Test "Just After" is always larger than you would think, a point many people miss. Refactoring for testability can save the day. Looking forward to the next installment.

    ReplyDelete