January 7, 2011
Classic TDD or "London School"?One question I get asked by teams adopting Test-driven Development is "should we use the 'Classic' or the 'London school' of TDD?"
Before I set out my answer, I should probably explain what I think is meant by "Classic school of TDD" and "London school of TDD".
If you've read books like Kent Beck's excellent Test-driven Development By Example, you will be familiar with the "Classic school". What distinguishes this school is the emphasis on a technique called triangulation.
When Classic TDD is illustrated, it's usually an example where an algorithm is fleshed out one test at a time. For example, if test-driving an implementation of, say, a program that converts integers into roman numerals, we might start with the simplest test "roman numeral for 1 is I". The simplest solution might be to return the hardcoded literal value "I". Then we move on to the next easiest failing test.
Test #1: 1 = I -> return "I"
Test #2: 2 = II -> if integer = 1 return "I" else return "II"
Test #3: 3 = III -> while(integer > 0) concatenate "I" to result and decrement integer
...or words to that effect. The idea here is that we generalise the solution one failing test at a time so we end up with a simple, elegant solution that satisfies all of the tests up to that point.
(In both schools, after we pass each test, we refactor the code to keep the design clean.)
And that, in a nutshell, is the Classic school of TDD. It's often seen as algorithmic in its emphasis. For anything other than the simplest, most obvious problems, we triangulate through a sequence of test examples and generalise the solution as we go until we've covered just enough test cases to produce the general solution. The OO design tends to grow from a single simple starting point, and classes, responsibilities and collaborations in an end-to-end OO design grow organically through the refactoring process.
The London school of TDD has a different emphasis, focusing on roles, responsibilities and interactions, as opposed to algorithms. It's born out of the furnace of component-based, distributed and service-oriented applications that are especially prevelant in the City of London (if you've got bail-out money to burn, why keep it simple, right?). The design emphasis of "enterprise applications" tends to be message passing as opposed to raw algorithmic computation. Of course, many would argue that this should be the emphasis of OO design, in any style of architecture, which is probably true.
The London school's definitive text is Growing Object Oriented Software Guided By Tests by Steve Freeman and Nat Pryce.
A very simplified description of this approach to TDD might be:
Identify the roles, responsibilities and key interactions/collaborations between roles in an end-to-end implementation of the solution to satisfy a system-level scenario or acceptance test. Implement the code needed in each collaborator, one at a time, faking it's direct collaborators and then work your way down through the "call stack" of interactions until you have a working end-to-end implementation that passes the front-end test.
Let's say, for example, that we're building a web application that users sign up for, providing a user name, a password and their email address. We have an acceptance test detailing what happens when the email address is not syntactically valid.
End-to-end, it might go something like this:
User Sign Up Page -> Sign-up Controller.openUserAccount(userName, password, email) -> Email Validator.validateEmail(email)
We may start by writing a test for the User Sign-Up Page using a mock Sign-up Controller with an expectation set that openUserAccount() will be called with a duff email address, and that the controller specifies that the response of the application should be to re-display the User Sign-Up Page with an input error message.
When we've passed that test, we might write a test for Sign-up Controller.openUserAccount() using a mock Email Validator with the expectation that validateEmail() is called with the email addressed passed into openUserAccount() (which we know is duff) and that it will return "false", at which point we check what the controller does with a "false" response.
And then we could write a test for the Email Validator that checks that when we call validateEmail() with a duff email address, it returns "false".
In some respects, this approach to TDD is slightly evocative of squeezing sausage meat through a sausage. We introduce some meet at the top, tie it off (one working sausage) and then we squeeze the meat down one level, and then down to the next level, and until we have sausage meat all the way along and a nice string of juicy, succulant enterprise sausages. (Mmm, enterprise sausages).
Folk who are building distributed "enterprise" applications often tell me that the London school is best suited to what they do, and I see the evidence of that in their heavy reliance on mock objects and on the lack of triangulation in their tests.
But, going back to the User Sign-up example, when we get to the Email Validator and our test case for a duff email address, is that really one test, with a simple and obvious implementation?
This can often be where TDD comes unstuck. All too often, developers make leaps in a single test case.
Consider these duff email addresses:
They're all invalid email addresses, but for different reasons. That word "invalid" can cover a multitude of sins.
The temptation is to knock up something using a regular expression that tries to enforce all of these rules to pass a single test case. essentially, our test tests more than one thing, which is a definite no-no.
Also, for each rule, there isn't always an obvious implementation. It could serve us well to tackle this in the smallest steps possible, which means working one rule at a time, and for the less obvious rules, triangulating to a solution.
My experience has taught me that an unswerving focus on interactions can lead to algorithmically unreliable code (bugs, basically). Likewise, an unswerving focus on algorithms can lead to a rigid and brittle end-to-end design, and sometimes to a failure to satisfy the system-level requirement.
Our goal here is reliable code and good internal design, which means that both schools of TDD are at times necessary - the Classic school when we're focused on algorithms and the London school when we're focused on interactions. Logic dictates that software of any appreciable complexity cannot be either all algorithms or all interactions (although, to be fair, I have actually seen applications like these - and wish to never see such horrors ever again).
This is why my TDD training courses have one day of "Classic TDD" and another day of "London school" TDD. It's not an either/or decision.
Posted 6 years, 1 month ago on January 7, 2011