by Adam Petersen, June 2011
This year marks the 10th anniversary of my Test-Driven Development (TDD) career. It's sure been a bumpy ride. If a decade of experience taught me anything, it is that the design context resulting from TDD is far from trivial. TDD is a high-discipline methodology littered with pitfalls. In this article I'll look at the challenges involved in introducing and teaching TDD. I'm gonna investigate something we programmers rarely reflect over, the form and physical layout of our code, and illustrate how it may be used as a teaching-tool.
The Human Parser
Did you ever think about how fast a decent programmer assesses the quality of any given piece of code? In most cases it's a matter of seconds. But, in that short frame of time, what is it that we actually assess? What thought-process do we rely on? Do we parse the code at hypersonic speed in our mind while rapidly calculating the cyclomatic complexity as we go along and arrive at a formally well-founded decision? Probably not. And if we don't, why does it matter?
The Good, the Bad, the Java
Last year I held a couple of workshops with the aim of introducing unit tests and TDD in Java. Since I do believe TDD to be a design technique with a potentially huge payoff (at least for most statically typed OO languages), I always start with the benefits. After all, it's like I got something to sell. In an inspired moment of pedagogical high I even launched a Common Lisp REPL (Read-Eval-Print-Loop) to demonstrate the benefits of interactive development that TDD enables. Yet, as reality hit the fan, without any mentors aboard, the TDD experience of the team turned out to be far from the rosy development dream my presentation hinted at.
I've seen similar failures of applying TDD before. Not only does TDD require a tremendous amount of discipline. It also immediately highlights flaws and insufficient design in the code under test. That immediate feedback, while praised as the big-win of all lean technologies, at the same time, it poses part of the main problem with TDD. How come?
Well, a developer starting with TDD always has a history. It probably ranges from more or less successful projects, but somewhere along the line we've all managed to deliver some code that actually works (at least for some definition of "works"). Put in other words: we all know how to program. As we start with TDD, the initial response is a lot of problems slowing development to a crawl. Complicated set-up code, tricky logic not easily expressed in a test, private data I cannot access from a test, heavy dependencies towards third-party products like databases and GUI, etc. Faced with all these obstacles, the testcases quickly degenerate and TDD is seen as quicksand upon which the team is trying to build a project. Pretty soon, the testcases are commented away or even shutoff in order to "deliver". More time passes and all that remains of TDD is a collective memory of a dysfunctional technique. A bridge is burned.
Obviously we all recognize the problems above as symptoms of flawed designs caused by the neglect of solid design principles. After all, feedback in any form is good only if acted upon. Perhaps TDD is best seen as a messenger; if something's hard to test we don't have a testing problem but rather a design problem. Is a team ever going to succeed with TDD, the design skills of the programmers on the team have to be raised. And this is the actual point where I deem a mentorship absolutely crucial to success. I mean, the mechanics themselves behind a unit test are trivial: tag certain functions as testcases, express the observable behavior of the system as assertions and press a button. If the design aspect was as simple as that, Fred Brooks wouldn't have written an entire book about it and we would probably all be writing dry business code in some cobolesque language by now.
Unit tests mirror the quality of the code they're testing. It's virtually impossible to separate unit testing and design. So what methods and techniques are useful to a mentor in guiding developers towards maintainable unit tests and thus a sound underlying design?
Some years ago, I had the opportunity to work with a group of mentors. Our goal was to develop a conceptual framework intended to make the transition to TDD as smooth as possible for the organization. A key part of the framework was a set of rules for unit tests. The rules were never an end in themselves. Rather, their purpose was to stimulate a discussion between developer and mentor. Each rule reflected one fundamental design principle concretized and augmented with specific examples from actual production code. We were deliberately oversimplifying. To give one example, the rules prohibited the use of explicit conditional logic a la if-else in the unit tests. The rationale was that an if-else -chain probably hints at low cohesion; the testcase probably covers multiple responsibilities and these should be separated. More often than not, it mixed normal flow of control with exceptional cases.
The rule set basically served as a learning tool and was never intended to live on. Come the day when we all understand why the rules are there and the organization has reached a point where it is actually okay to break the rules.
While we did have some success with the idea I continued to look for ways of further simplifying the rule set. There's something about enforcing rules upon my fellow programmers that I never quite liked. Besides, I had the idea there had to be some common, alternative way of capturing the rules.
Let's return to the original question asked at the beginning of this article: no, we don't really parse code with hypersonic speed in our mind when determining its qualities. Our brain has a much more powerful tool: the visual system allows us to process a tremendous amount of information at literally a glance. What we actually do is comparing the physical, visual shape of the code against our experience. And even if we aren't consciously aware of it, years of coding has taught us how good code looks in our beloved Emacs buffers.
Have a look at the following two code layouts. Both of them reflect the form of a small unit test suite. Which one of them would you prefer to maintain and extend?
Test suite 1
Test suite 2
Have you chosen your favorite? Here's my take on it. Independent of programming language, the differences in complexity are quite striking and immediate. And that visual contrast serves as a tool for discussing design and highlighting basic principles. It's not limited to the educational aspect; I've found that the patterns work well during code reviews as an effective way of encouraging discussions about a given solution. If we take a high-level view, what's the shape of this very piece of code? Are there any parts of the design that diverge from the rest? Any signs of growing complexity? If so, why's that and is there a better way to attack the problem?
Obviously there are many valid reasons to design differently and the trade-offs may indeed motivate and result in radically different visual shapes than the ones above. The challenge is to make it an active decision rather than an ad-hoc solution. At the end, it's all about reflecting upon current practice, sharing knowledge within the team and continuously improve. And if you manage to get a team to actively discuss different design aspects on a daily basis, you're halfway there.
Test-Driven Development is a powerful yet unforgiving design technique. In order to adress the design flaws that TDD inevitably highlights, it's important to recognise them as such and to act upon the feedback. As presented above, visual code patterns serve as a tool for discussing designs, perhaps by collecting a small library of patterns for different kinds of code and programming languages. A first step would be to print out the examples, tape them to the walls and let them compete for attention with the obligatory Dilbert strips. Discussing and comparing different code patterns together with other programmers and mentors provides an excellent learning opportunity.