Books : reviews

Steve Freeman, Nat Pryce.
Growing Object-Oriented Software, Guided by Tests.
Addison Wesley. 2010

rating : 2.5 : great stuff
review : 4 December 2011

Kent Beck's Test Driven Development: by example is a great tutorial on how to do TDD: do we need another?

Well, yes. Beck's book is good, but it looks at the process from one direction: writing small tests, writing the simplest possible code to make them pass, then refactoring to clean code. Here Freeman and Pryce take a more systems-level approach: writing tests as part of the design process, test that help grow the overall code into the desired system. They have their own approach to this process, making heavy use of "mock" test objects (rather than having to have the surrounding implementation in place to run a test), which also supports their particular style of iteratively adding code ("growing" it) to make a test pass.

The first part of the book explains their object-oriented design and development philosophy (for example, for query methods: ask the question we want answered, rather than asking for the information to let us figure it out; also build new functionality by developing from the inputs to the outputs, which gives a uniform development style that iteratively discovers and implements the needed services), and how their testing process can help support this style. In particular, the TDD approach helps the design process. Whole system level acceptance tests specify what functionality is to be built (and since they specify what, rather than how, they can be written in a higher level, declarative style), and can help stop any unnecessary code being developed.

p7. When we're implementing a feature, we start by writing an acceptance test, which exercises the functionality we want to build. While it's failing, an acceptance test demonstrates that the system does not yet implement that feature; when it passes, we're done. When working on a feature, we use its acceptance test to guide us as to whether we actually need the code we're about to write---we only write code that's directly relevant.

Unit testing tests the underlying objects in isolation, and also helps the design process. (Of course, objects are not truly isolated: in an application they communicate with other objects to fulfil their responsibilities. This is where the mock objects approach comes in, here illustrated with the jMock2 framework.) The philosophy is the same higher level one: unit test behaviour, not methods.

p11. Thorough unit testing helps us improve the internal quality because, to be tested, a unit has to be structured to run outside the system in a test fixture. A unit test for an object needs to create the object, provide its dependencies, interact with it, and check that it behaved as expected. So, for a class to be easy to unit-test, the class must have explicit dependencies that can easily be substituted and clear responsibilities that can easily be invoked and verified. In software engineering terms, that means that the code must be loosely coupled and highly cohesive---in other words, well-designed.

The tests also help document the system design. The code captures only the class structure; the tests can capture and make visible the dynamic structure of communication patterns (or protocols).

p15. these communication patterns … are what gives meaning to the universe of possible relationships between the objects. Thinking of a system in terms of its dynamic, communication structure is a significant mental shift from the static classification that most of us learn when being introduced to objects. … the communication patterns are not explicitly represented in the programming languages we get to work with. … tests and mock objects help us see the communication between our objects more clearly.

[This has resonances with complex systems and emergent properties: the communication patterns are emergent (since not explicitly represented the the code), and the communications are "more important" than the "things" doing the communication.]

Adding acceptance tests for new functionality is all very well, but how do you get started, what the very first step? The authors make used of what is memorably called a "walking skeleton":

p32. First, work out how to build, deploy, and test a "walking skeleton," then use that infrastructure to write the acceptance tests for the first meaningful feature. ... A "walking skeleton" is an implementation of the thinnest possible slice of real functionality that we can automatically build, deploy, and test end-to-end

So there is always a working system: not just working in terms of its functionality (no matter how limited in the early stages), but in terms of an automated build and deploy cycle, too. This pragmatic software engineering approach carries on through the book. The second part is an extended case study, where we get to see how to grow a non-trivial application using system-level TDD to support the authors' OO design style.

And finally they discuss some more advanced things (the way state transition diagrams map directly onto tests; to use domain-specific types rather than domain-neutral types such as collections and strings; how test code tends to have concrete values and an abstract "how", whereas production code tends to have abstract values and a concrete "how"), and things that make testing difficult, like persistence, threads, and distribution.

Also of interest is Tim Mackinnon's Afterword on the history of Mock Objects: how the pattern was noticed, gradually refined, and implemented into a powerful testing framework. It's always good to see the genesis of these powerful ideas: they didn't spring fully formed from someone's brain, but developed slowly, iteratively, through practice, over the years.

This is an excellent book, full of good sense about object orientation, about design, about testing, and about developing solid code that the user wants. Recommended.