Monday, April 2, 2012

Really TDD-ing: Less simple than you think

TDD is a pretty simple system, as popularly described.  For an individual doing a kata or playing with some example code it is really just three steps. On card #44 of AgileInAFlash it looks like this:

It's the right way to think about TDD, and the right way to get started.

When you get to a real project in a team environment you have many other issues to consider.  In a modern team environment, with a DVCS, it looks more like this:
  1. Get the current code (git pull, get clone, hg pull -u, whatever)
  2. Run all the tests and be sure they're all really green, to avoid blaming yourself for unrelated breakages.
  3. If there are no tests, write a trivial failing test ("assertTrue(false)") to prove your testing setup (ide, scripts, makefiles) really work. Then delete the trivial test.
  4. Write one "real" red test. INSIST ON RED.
  5. Write code to turn it green.
  6. Local commit "save your game"
  7. Refactor (or intentionally choose not to).
  8. Check to be sure you're still green.
  9. Local commit.
  10. Push your change to the shared repo (or have a darned good reason not to).
  11. Start from (1) again.
The first two steps are really important, because our peers may have checked in code without being sure that all the tests pass. We don't always work with perfect Agile coworkers (especially in a company with multiple teams, or one in transition, or both).

If you don't commit on either side of a refactoring (steps 6 and 9) you can quickly end up with no recourse but to revert and lose minutes or hours of work. Because the hard part of programming is the learning and thinking, losing code is really a triviality but it isn't fun to have to retype the code you have already built once.  It's good to have a known "green" state to return to, and the more recent the better. 

There is more to the flow than I listed, though. If you get a red test, you have to back up a few steps. If you did anything significant, you will want to push to the main code line after each refactoring and possibly after each green bar (steps 9 and 6, respectively). 

It's often wise to put the "real" code to turn the test green (step 5) into your test class initially, and then migrate it to the production code during the refactoring break (step 7). It keeps you from having to bounce around between a bunch of files and tightens your cycle considerably.

Since you are pair-programming -- as sensible people do -- you might switch pairs at every red (step 4) or every green test result (step 5). That's a small ritual that keeps everyone's head in the game. 

If you have a messy code base and/or a refactoring that touches code in a great many places, you may want to do the refactoring (step 7) in the fresh main line code, and then pull the refactoring back to your local copy. Trying to keep large refactorings in a branch will cause a lot of integration pain. Best to get it to everyone soon, and get it over with.

If you use feature branching (God help you) then everything gets harder to do, and you'll feel a pressure to avoid pulling code updates and rerunning tests (steps 1 & 2). Avoiding them makes it worse. The longer you're off the main code line, the worse all your merges will be. Get back on soonest.

If you don't have a DVCS (again, God help you!) then it all gets rather messy because non-distributed version control means you can't have a local commit. Some people say that's better, but I say it ain't*. A remote commit takes a lot more time than a local savepoint, and having dozens of them at once, every minute or two from every pair, can drag a reasonably good server to its knees.

* Apologies to Billy Joel, and of course my 4th grade English teacher.