TDD in Three Parts
Test-Driven Development by Example is broken up into 3 parts, each with a different focus. A quick introduction to the TDD process (red, green, refactor) kicks things off. For those unfamiliar with TDD, the idea is that we always write the tests first. So, the first thing we do is write a failing test (red). Then, we write just enough code to get that test to pass (green). Finally, we eliminate duplication and/or rework the design (refactor) to make the code that was "just enough to pass" into code that we don't mind leaving in the system.
Part I: The Money Example
The first part walks through an example of creating a way to manage money with different currencies (in Java). I found this section to be the most useful. Beck starts by creating a written list of things that need to be tested based on the functionality required.
In the money example, this comes down to the following:
$5 + 10 CHF = $10 if rate is 2:1
This is where we want to end up, but it's way to big of a step. So, the test is broken down into smaller and smaller steps that we can actually write tests for and code for.
I won't got into the details here (that's why you should read the book). Ultimately, to get to this final functionality, we end up with a collection of 30 or so tests (sorry, I didn't actually count them). This may sound like a lot, but it turns out to be worthwhile. Throughout the process, the focus is getting small pieces of functionality into the system (even if it is just verifying equality).
The result of the continuous red/green/refactor process is a design the evolves based on the actual needs of the system rather that what we think the system might need.
Part II: The xUnit Example
The second part is equally interesting; Beck builds a unit testing framework with Python. Python is used because of the dynamic nature of the language. It seems a bit meta to be building a unit testing framework using TDD (and granted, it seems a bit awkward getting started). But ultimately, building a test system based on its ability to test itself is intriguing.
The actual example may not be that applicable (most of us don't build unit testing frameworks). But the process is extremely useful. Again, Beck takes the process and breaks it down into small, easily testable steps. This keeps a constant forward motion -- we don't get "stuck" anywhere along the way.
Part III: Patterns for Test-Driven Development
The third part is more a catalog of different patterns that can be used to help with TDD. This includes both patterns that are used for the tests themselves as well as the patterns used in the tested code.
The testing patterns are those used throughout the book, so it's good to see a separate (sort-of-formal) explanation along with the example from the earlier code. This serves to reinforce the ideas from the earlier parts of the book.
There is a chapter on Refactoring which is both good and not so good. It's good in the way that it shows different refactoring patterns (such as Move Method and Extract Interface) along with steps that we need to take to implement the pattern. But it is a bit anemic. Most of the patterns just have a page or two of description.
This isn't a book on refactoring, so I wouldn't hold the lack of details against it. Instead, treat it as a good jumping off point to study refactoring in more detail. A great choice for refactoring patterns is Working Effectively with Legacy Code by Michael C. Feathers (recent review). Since I recently read this book, the refactoring patterns were fresh in my mind. Another recommendation that comes up is Refactoring: Improving the Design of Existing Code by Martin Fowler and featuring Kent Beck, among others (Amazon link). As a disclaimer, I haven't actually read Refactoring yet (it's in my stack of "to be read"), but it gets mentioned so often (and it's Martin Fowler), that it's a pretty safe recommendation.
What I Took Away
So, what did I take away from Test-Driven Development by Example?
Don't Think "Too Big"
First, I've been thinking "too big". What was interesting about both examples is how Beck continually broke the problem down into smaller and smaller pieces. My tendency is to think of the problem at the macro level and try to test for that. Instead, I need to think much, much smaller.
The idea behind writing small tests is to keep from getting stuck. If you come across a problem, and you're not quite sure how to approach it, break it down into simpler pieces. This way, you are constantly moving forward. With immediate feedback (watching the red turn to green), we are constantly making progress.
In several places in the book, Beck starts taking larger steps (when he's feeling confident). Sometimes those larger steps work; sometimes they don't. If the larger step doesn't work, then there's no problem. Simply break it down and try again.
One really good piece of advice that Beck gave is to leave a "red" test at the end of a coding session. This will serve as a bookmark when we pick up your coding the next day. And it will help us get back into the mindset of where we left off.
Test Until Bored
Another problem is to figure out when we might be thinking too small. Maybe we get into a position where we think our tests are getting too small -- that we're testing too much. Beck recommends that we "Test until bored."
"Test until bored" is a great approach. Again, it helps to make sure that we remain interested in our code and what we're building. I've heard this repeated by a few different testing folks, including Llewellyn Falco in a recent episode of Hanselminutes (Approval Tests with Llewellyn Falco). This is an excellent podcast, and Llewellyn shows how Approval Tests (an open-source testing "enhancer") can sit on top of your current testing framework to make it easier to test things that would normally be hard to test.
I do still have one open issue after reading Test-Driven Development by Example: how much design do we do?
If you've seen my previous posts about design patterns (available here: Learn the Lingo: Design Patterns), you know that one of the reasons I encourage people to learn design patterns is so that we can stay in design mode longer (meaning, we think about our design a bit more before writing any code). This is the exact opposite of what TDD tells us. TDD tells us that if we write our tests in small increments and refactor appropriately, then good design will naturally evolve.
I'll have to admit that I'm still stuck on this one. As an experienced developer, I've got a fairly good idea of what needs to be built in certain circumstances. In those cases, I end up with quite a bit of the design already done by the time I start coding. This may be something that I need to unlearn. Or it may be something that I need to reconcile with TDD. We shouldn't give up what we've learned through our experience. So, probably the best approach is for me to learn how to combine TDD with the design that I think I'll end up with. It could be that TDD takes me in a completely different direction. Time will tell.
A lot of experienced developers recommend TDD as a way of building well-design, maintainable apps. It's something that's really hard to ignore. It helps us write code in small pieces. As I recently heard, "If you aren't writing incremental code, then you're writing excremental code." Test-Driven Development by Example gave me the information that I need to jump in and do this in my own coding. Seeing the examples play out in a step-by-step fashion helped me understand the TDD mindset of moving in very small pieces and only building things as the tests demand.