AmanKing

TDD by ExampleControl PanelChange LogBrowse PagesSearch?

TDD by Example

After Steve, a fellow-TWer, strongly recommended TDD by Example by Kent Beck, I got a copy off our Pune office library. Steve set high expectations for the book in my mind: it changed the way he thought about programming; the book touched topics beyond TDD, giving reasons to the principles; reading the book was like pair programming with someone very experienced.

Now that I've finished reading the book, I'd join Steve in the book's fan club. Having practised TDD in my last ThoughtWorks project, and having paired with and learnt a thing or two from Steve, there were less surprises for me in the book, but there certainly were some "Aha!" moments. I want to share those here:

Aha! 1

The first thing about the book that anyone will notice is Kent Beck's characteristic style of writing: totally informal and conversational. His jokes here and there would make me jump up with laughter, making my wife doubt that I was reading a technical book. You can breeze through the short crisp chapters in one continuous flow, all the while recording learnings in your mind in the form of advice from a friend. You can't say this for most techie books out there.

Aha! 2

Kent identifies three factors that help create or add business value in a software project:

  • Method: The team needs to know the mechanics for making the necessary transformations. Experience in evolving the system's design helps.
  • Motive: The team needs to understand the importance of a feature or change that needs to be implemented. They also need the courage to go ahead with it.
  • Opportunity: A combination of comprehensive, confidence-generating tests, a well-factored program, and a programming language that makes it possible to isolate design decisions means there are few sources of errors and those errors will be easy to identify.

Out of these, getting the motive that will multiply the value of the project may not always be under our control but the other two factors definitely are.

Aha! 3

Until this book, I always saw TDD as red-green-refactor. This has worked well for me. It's definitely much better than just red-green minus the refactor, which is a trap we must be wary of when working under pressure. However I always thought of the refactor step as refactoring within production code; I never saw it as a starting step of removing duplication between production code and the test around it. But as Kent describes the process, the refactoring actually starts off as a simple step of removing hardcoded values that are making a test pass.

According to Kent, the two simple rules of TDD are:

  1. Write a failing automated test before you write any code
  2. Remove duplication

As Kent puts it, the book is about the subtle gradations in applying these rules, and the lengths to which you can push them.

The TDD cycle that Kent mentions more than once throughout his book is as follows:

  1. Add a little test
  2. Run all tests and fail
  3. Make a little change
  4. Run the tests and succeed
  5. Refactor to remove duplication

Steve Freeman points out that the problem is not the duplication itself but the dependency between the code and the test: one cannot be changed without changing the other. The goal is to be able to write another test that "makes sense" without having to change the code.

This is what leads us to a good simple design that's better than just hardcoded values that make a test pass. By eliminating duplication before we write another test, we maximise our chance of being able to get the next test passing with one and only one change.

Aha! 4

Before you dive into writing tests one after the other as they come to mind, make a to-do list. Pick the test you want to write next from the list; if you think of something new that should be tested (or test-driven rather), put it on the list rather than do it immediately. This will help in picking the right order for implementing the various tests, such that you manage scope while making good progress in small confident steps, rather than start on tests that may turn out to be too big to chew off initially.

Speaking of tests that are difficult to chew off, it's okay to leave them midway and write smaller tests that would help make the bigger one pass in a little while. But remember not have more than one red bar at a time. The suggested way is to delete the bigger test for now and come to it later (although it's forgivable to simply prefix it with an "x" so that it doesn't get run until you are ready for it).

On similar lines, if you and your pair start a discussion around a test that you're trying to pass and the discussion hops on to another seemingly related topic, it is wise to keep the following rule in mind: entertain a brief interruption, but only a brief one, and never interrupt an interruption (this rule comes from Jim Coplien). This most of the times happens when you're thinking of refactorings alongside test implementations.

Aha! 5

The goal is clean code that works.

The pithy summary that comes from Ron Jeffries not only identifies what all pragmatic programmers strive for but also hints at the approach that most programmers take when solving a problem and what approach should actually be embraced.

As programmers we are generally very personally tied to our work and want to make it beautiful. We design away as if conceptualizing a master piece before picking up a brush. This happens when programmers aim for the "clean code" part of the goal first.

Kent Beck suggests that we target the "that works" part first. The direct benefit is that once we have something that works we can courageously try to make it cleaner, ie, tackle the "clean code" part of the goal. If we do it the other way round, we run the risk of creating elegant solutions that do not solve the given problem.

Moreover when we're solving the "that works" part, our discussions are around how the system should behave and what is acceptable to the user. If we tackle the "clean code" part first, we digress into "airy-fairy" discussions about "beautiful code", aesthetics, flexibility, and what not.

So, although the goal has both aspects to it, it is important to tackle the parts in correct order. (Chirag, a fellow-ThoughtWorker, pointed out that it's important to note that the time between doing the "that works" part and subsequently the "clean" part must be short and in small steps; if not, we risk accumulating technical debt.)

Aha! 6

The following are ways to make a test go green (it's nice to finally have names for them):

  • Fake It ('Til You Make It): The first implementation to a broken test is to return a constant. Once the test is green, gradually transform the constant into an expression using variables.
  • Obvious Implementation: If the operation you are trying to implement is simple enough and you are confident of it, just implement it. But do note that solving "clean code" at the same time as "that works" may be too much to do at once. If you feel stuck or get into a vicious cycle of "oh, it's breaking because of just one small thing" and you hear yourself saying that again and again, take a step back and use Fake It to arrive at your solution gradually.
  • Triangulate: Only abstract or generalize the solution when you have two or more examples. Start off with returning a constant but before you generalize, add another test for the same operation but with different arguments. Triangulation helps when you're really unsure of an implementation; for something more obvious, Kent prefers Fake It or Obvious Implementation. Personally, I like Triangulate as it gives me additional confidence about my implementation (though redundant) whereas in the other approaches, you may have just one assertion around the operation.

No matter what approach you take, the rhythm of red-green-refactor must not break. Sometimes teensie-weensie steps slow you down while other times implementing large solutions in one go may hinder the pace. The key is to adjust often and try different ways of making a test green depending on the test case.

Aha! 7

Privacy is important. Encapsulation. Do not compromise it for tests. All tests should be written using only public protocol of the class under test. Wishing for white box testing is not a testing problem, it is a design problem.

You may start off by asserting on a certain field of an object but try to quickly convert such tests into assertions of equality between entire objects: expected and actual, rather than on individual fields.

This means that your test now depends on the reliability of the class's equals() method even though equality is not what you're testing. This is an acceptable risk as long as there are tests that specifically test the equals() method of the class.

I remember Chris Stevenson, a senior TWer, mentioning that he thinks an equality test is a great first test for any class.

Aha! 8

Even excellent programmers tend to spend 5-10 minutes reasoning about a question than can be answered by the computer in 15 seconds. Without tests, you have no choice but to reason. With tests, an experiment may answer faster. Sometimes you should just ask the computer.

Personally I've seen this too, when my pair wants to talk out things like various approaches, how an API would look, what they'd need to do, and so on, before writing any code. I get impatient and I just want him to try test-driving it out so that I can understand what he wants to do by looking at his code rather than imagining it. This is not to say that we should code away without any forethought... this is to say that forethought should go into writing communicative tests rather than abstract discussions.

Aha! 9

Kent mentions how Ward once came up with a great "trick" that helped solve a certain problem, a trick that he hasn't seen anyone else come up with independently. He then goes on to say, "TDD can't guarantee that you will have flashes of insight at the right moment. However, confidence-giving tests and carefully factored code give you preparation for insight, and preparation for applying that insight when it comes."

That's a powerful statement. And it's a direct answer to a discussion that Steve and I had about a discussion that Srihari and Sachin had. (Yeah, in ThoughtWorks, we have a lot of discussions and meta-discussions.) The discussion was whether TDD directly guides you to better OO programming or not. I personally am borderline and would go with what Kent has said above. One way that tests help is by becoming very painful to write or understand when the object modeling becomes awkward.

Kent also goes on to say: "You could write the tests so they each encouraged the addition of a single line of logic and a handful of refactorings. You could write the tests so they each encouraged the addition of hundreds of lines of logic and hours of refactoring... The tendency of TDDers over time is clear, though -- smaller steps."

Aha! 10

More names for concepts! The components of a typical test -- 3A from Bill Wake:

  1. Arrange: create some objects
  2. Act: stimulate them
  3. Assert: check the results

Aha! 11

There is no simple relationship between test classes and model classes. In other words, there need not necessarily be only one test class per model class, which till now I took to be the norm!

If we are using test classes to represent fixtures, then all tests sharing the same fixture will be methods in the same class. Tests requiring a different fixture will be in a different class. Sometimes one fixture serves to test several classes (although this is rare). Sometimes two or three fixtures are needed for a single model class.

So, before my "TDD by Example" days, I'd create (and expect) only RectangleTest to hold all kinds of tests related to the Rectangle class. But post "TDD by Example", for Rectangle, I'd probably end up with EmptyRectangleTest, NormalRectangleTest, and so on.

Aha! 12

Value Objects. Once created, these objects are immutable. Every operation on them returns a new instance. A great way around aliasing problems, ie, someone asking for one of your data members that you depend on and changing it behind your back: share only value objects with others.

Note that all value objects should implement equals().

Aha! 13

OO programming is about modeling the real world as objects. However, some design patterns do not make sense in the real world although they are still a good idea because the benefits are enormous making the conceptual disconnect worth it. Composite pattern is a good example: a folder containing folders (a real-world concept tweaked to represent a computer file system hierarchy); a collection of accounts acting as a single account (allowing a real-world "Multiple Accounts Summary" to work seamlessly as if it were an individual account); etc.

Aha! 14

Miscellaneous good TDD advice.

What don't you have to test? Phlip replies, "Write tests until fear is transformed into boredom." Write tests for code that you write. Unless you have a reason to distrust others' code, don't test that.

Some test attributes that suggest a design is in trouble:

  • Long setup code
  • Setup duplication
  • Long running tests
  • Fragile tests

Paradox: By not considering the future of your code you make your code much more likely to be able to adapt in the future.

When should you delete tests? If you have two tests that are redundant with respect to confidence and communication, delete the least useful of the two.

Conclusion

Those were a LOT of "Aha!" for someone who started off by writing that he didn't find many surprises in the book!

Something to note here is that most of the points I've called out were hidden somewhere in the "conversations" that Kent has with you, his reader, while driving the TDD example. They are not sections or subheadings by themselves but just friendly advice or experience-sharing. This is where the beauty of the book lies. So, go read it now! Even if you're read it before. I'm sure you'll find your own great list of nuggets of wisdom.


Tags: technology:books Last modified 03:29 Thu, 18 Sept 2008 by AmanKing. Accessed 112 times Children What Links Here share Share Except where expressly noted, this work is licensed under a Creative Commons License.