Wednesday, November 3, 2010

Test features, not implementations

I used to try to test my code. I would fire up some JUnit classes and go to town, trying to think of what parts of my code were testable. Having read a bit online about unit tests I would try to do all the things I was reading about - test public method signatures, parameter validations, and even do some wiring tests making sure my code was calling my other code in the ways I expected.

However, at the end of the day, I just didn't see all that much value in writing these tests. They did help a bit, and often made writing the actual code easier if I was dedicated enough to test drive. But it came at a high cost - not only did I have to spend time writing the tests, I had to spend time maintaining them as well. Every time I changed code I had written before, I had a horde of broken tests to deal with.

I really wanted to figure out this whole testing thing, but it just wasn't making sense.

Enter Business Driven Development (BDD). It was like a veil was lifted and I could see what I had been missing before. You still wrote tests, and wrote them first so you were still test driving your code, but instead of testing all the little details of your code, you tested features! It made so much sense.

Now, what is a feature you might ask. I define a feature as anything your application is supposed to do. If you click a button and an overlay is supposed to open, that's a feature. If the overlay is supposed to display an image, that's a feature. If you click a close button and the overlay closes, that's a feature.

Let's assume you have a jQuery plugin named "overlay" that, when called, opens an overlay. Let's also assume you're using Jasmine for your JavaScript tests because it's awesome and BDD oriented.

Here's the wiring test for opening the overlay:

it('should call the overlay plugin', function() {
  spyOn($.fn, 'overlay');
  $('button').click();
  expect($.fn.overlay).toHaveBeenCalled();
});

Here's the feature test for opening the overlay:

it('should open an overlay', function() {
  $('button').click();
  expect('.overlay:visible').toExist();
});

They might not look all that different, but the difference is very significant.

If you wrote the wiring test, it would make sure that your plugin was indeed called. However, what if you wanted to refactor your code later? What if you needed to change the overlay plugin to only configure the overlay but not open it? Your wiring test would need to be rewritten. What if you had ten of these wiring tests? Hundreds? Any refactoring you do would cause a ton of test maintenance!

Now look at the feature driven test. What is it really testing? The user clicks a button, and and then an overlay should be visible. There's your exact requirement from your customer and there's the test that makes sure it works. Refactor your heart out, the test can stay the same because you're testing your feature, not your implementation.

This is a very simple example, but the concept only gets more important as the features get more complicated. Your tests should always work for you instead of against you, and letting you refactor without needing to change your tests is a huge win.



P.S. I'm not saying you shouldn't ever write wiring tests, but I would recommend using them as plan B. They can still come in handy (e.g. when integrating with libraries that you can't control), but I would recommend against writing them for everything.