Saturday 1 September 2007

Interaction vs stateful tests, or Falling for Need-Driven Development

A few days ago I started adding paging functionality to FreshReports, and immediately fell into a trap some people call Need-Driven Development (see also this paper). Actually it's not a trap but rather one of thу possible strategies in TDD. However, I felt trapped for some time until just now -- I decided to rewrite my paging functionality using a more stateful approach.

So, how do you do it with NDD? First, you formulate the story, as always. Mine is, "Suppose we have two sections in our report, then if the second doesn't fit on a page, it goes to the next page". Next, I start to code the Page' s CalculateLayout method, something like this:

For Each section In sections

...

If section.Fits() Then

'do something about it


So, we want to code the CalculateLayout method and not get distracted by the Fits method, which is probably a lot to code, so we just mock it out and get a green on CalculateLayout. Next we switch to the Fits method which probably depends on something else, so we mock this something and proceed with the Fits method until it's perfect.

This technique seems fine from a traditional development's point of view. In fact, it is more or less design-first approach. But when I tried to move this way, after programming the TDD way for some time, I was feeling very uneasy. In the TDD fashion, I should have made a simple test that covers everything (section setup and printing), and mocked just the call I am sure of (to the actual element printing method) (note that I could have mocked the call to the section's Print method instead and save time on adding elements to the sections, but only if I'm sure that it works fine with paging). With NDD, I had to write tests for several steps, and since I don't see the whole picture from the start, I don't even know how soon I get to the actual printing. With TDD, I would have let my tests do the design for me, and even while writing the first test I would have discovered that I need a separate Pager class. With NDD, I sort of know the design of the current step before I start writing the test, and that's wrong, because, for example, later I discover that my Fits method needs two additional arguments, and changing its signature will break my tests.

Why have I fallen for it? Partly because I just read an article in which some obviously smart guys told me it's good. Partly because it's tempting. In my case, while writing the first test, I didn't know how to setup this "doesn't fit" idea, so I just mocked it. It was quite easy to write a test, even easier to write the production code, but after that I didn't feel I achieved something, quite the opposite, I felt that I've just made things more complicated. So you see, it's all about feeling good or bad, so it's quite personal.

So, is it that bad? Of course, not. NDD is great for more structured guys. I was feeling helpless because I didn't have a clear picture of what's happening, but that's just me. I prefer to do something quick in a very chaotic way, and then refactor it to make it more structured. NDD is for those who prefer to move step by step.

In fact, I'm using NDD when I encounter an external dependency, such as FTP or file system. I resist a temptation to just save a file, instead I code an interface and use it. I still have to remember to implement it later, but at least it's an end point rather than part of a long chain of objects. So, to avoid this temptation, I made a rule for myself: never mock an interface you haven't implemented yet, save for such thin implementations of external dependencies.

2 comments:

Jeremy Ross said...

It's interesting that you find NDD most valuable on the boundary cases -- exactly where the "Mock Roles, Not Objects" paper suggest you not apply NDD.

It seems that in any top-down design approach you can find yourself "trapped" in a design that's not implementable at the lower levels.

ulu said...

As I said, it's probably personal. A little more upfront thinking and a couple of diagrams would have saved me I guess, but I'm not that kind of guy.

I've been writing functional tests first since then, and the confidence it gives me makes it much better. I might skip some refactoring, but at least I'm sure that this stuff works.