[Book Review] Test-Driven Development By Example (a TLDR)
A few days ago I’ve finished reading Test-Driven Development by Example by Kent Beck. It is a great book and I thought I’ll share a few thoughts and notes on its content that might serve as a TLDR for other people. I’ll also add my own take here on some of the things discussed in the book.
TDD stands for Test-Driven Development and it is a software development practice that helps you write more reliable and well design software through the process of writing failing tests first and then coding the implementation that makes those tests pass.
Overall the book talks about TDD practice and showcases how to implement various programming tasks such as money converter/calculator and an xUnit framework using TDD. Another big thing that the book communicates is the mindset and approach when applying test-driven development: bite sized tasks, todo lists, and refactoring. In the last part of the book Kent Beck talks about TDD approach and answers various questions about TDD patterns, design patterns, who is TDD intended for, and so on.
I enjoyed reading it and I believe it is a must read for anyone who is practicing TDD or thinking about doing it. For former it will give some insights on what are the best practices of TDD and how xUnit frameworks work under the hood. For latter it will give an idea of the mindset you should have using TDD and what are the benefits that you get practicing it.
Read on if you want to learn more about what each part of the book covers.
Part 1 The Money Example
In the first part of the book Kent Beck shows us how to build a money calculator that supports configurable currency conversions. Examples are written in Java but as with any OO code applicable to other OO languages.
Interesting enough building a money calculator isn’t as trivial as might think. Especially when there’s currency conversion involved.
The author introduces an idea of “todo lists” for your TDD process. Essentially the idea is to break down not only tests that you want to write but also the features, refactoring, and implementation you want to do in a set of todos. As you go along your journey implementing whatever’s that you’re building instead of keeping it all in your head you create a list of super small tasks to do for your coding session and through it one by one crossing off the stuff you’ve done. That technique helps you work in bite-sized steps focusing only at the matter at hand. If while you were working on one of the items on your list you discover that there’s a refactoring that needs to be done or a test that needs to be added instead of jumping right in you add it to your todo list and get to it later when you can.
Also Kent Beck explains red-green-refactor cycle in this part of the book.
At the end of part one he lands on a pretty interesting implementation of money calculator/converter that has a concept of an
Expression that takes two arguments/”moneys” and computes them converting the result to the currency you specify. The implementation had a certain decorator/composite design pattern feel to it in my opinion.
Part 2 The xUnit Example
In the second part of the book Kent Beck takes us on a journey to implement an xUnit framework from scratch using Python.
It was very eye-opening to see how simple is an actual implementation of xUnit could be. After reading this part of the book you’ll have a better understanding and hopefully a greater appreciation for testing frameworks that we use.
Kent also continues to use his “todo approach” throughout this part of the book.
Part 3 Patterns for Test-Driven Development
In this part of the book Kent talks about patterns for TDD. It’s broken down into multiple sections such as Test-Driven Development Patterns, Red Bar Patterns, Testing Patterns, Green Bar Patterns, xUnit Patterns, Design Patterns, Refactoring, and Mastering TDD. Each section has several points, patterns, and advice on TDD in them.
Here’s a quick overview of what each section talks about:
Test-Driven Development Patterns
In this section the author’s talking about how stress level correlates with lack of tests, i.e. less tests you have the more stress you have because you can’t be sure if the changes you make are not breaking anything in your code. The more pressure you get from outside sources such as stakeholders the more stress you get, the more stress you have the less tests you write, the less tests you have more bugs and defects you introduce, the more bugs and defects you introduce the more pressure you have from your stakeholders. Vicious cycle. Break the cycle, start TDDing 😃
Test isolation is another point Kent touches upon in this section. The idea is that every test that you run should be isolated and independent from other tests. What that means in practice that there’s no lingering side effects in your system after you run each individual test.
Test list is essentially that “todo list” idea that Kent Beck is advocating for throughout the book. Before you decide to test you should create a list of all the tests you know you will have to write and as you go along add more to that list to implement later.
Test fist idea is the essence of TDD, before you write a single line of code you should write a failing test that will help you figure out what code needs to be written and what form and shape it should have.
Assert first is a more tactical advice on how to write individual tests. Start with an assertion, it will help you figure out what setup needs to happen for this test before it can be launched. After that it will fail and you can proceed with your implementation to make it pass.
Test data used in test should always be clear and to communicate use cases for test’s readers.
Red Bar Patterns
This section describes patterns that help you decide when to write tests, where to write tests, and when to stop writing tests.
One step test really means that there is not right order in which you write tests. Ideally each test is very small but teaches you something about the thing you’re building that you didn’t know before.
Starter test is typically the one that tests the obvious operation or an operation that does nothing. This way you cover your bases and get your red/green/refactor flow going.
Explanation test is a way to get other people onboard with TDD. Basically in your conversations with pears ask them to translate the logic of an API they are talking about into a test case.
Learning test is a test that verifies that an external 3rd party API or a library works as expected. Useful to check your assumptions on how a library works before starting to use it and is very handy verifying that everything still works as expected when you update it.
Another test is essentially the idea that that “todo list” of tests to write will keep you on track when discussion with your pair-programming buddy go astray.
Regression test is a test you write when a defect/bug is reported that simulates the bug. Make it fail first and then fix it. Make a note on why you didn’t see that scenario and test writing the code in the first place.
Breaks, **take them 😃
Do overs sometimes are necessary when you get stuck.
In this section Kent talks about more detailed techniques for writing tests.
Child test. When you find yourself writing a huge test with a lot of input and setup consider breaking it down into several smaller tests.
Mock object. Use fake objects to substitute those that do expensive operations such as database calls or networking.
Self shunt. Have your object under test communicate with mock objects or test objects (if you’re using xUnit) themselves to test the behavior. It is useful to test delegates and make sure the right messages were sent to a delegate but the object under test.
Log string. To test the sequence in which messages are called is correct use a log string. By appending to it the name of the method called you can then assert that your resulting log string is equal to a string that represents the right sequence you’re expecting.
Crash test dummy. Use fake objects to simulate exception edge cases.
Broken test. Leave a programming session with a broken test. Sounds counter-intuitive but it will give you a good indication on where to pick up when you get back to coding next time.
Clean check-in. A bit contradictory to the previous point - when you work in a team leave all of the tests running.
Green Bar Patterns
This section talks about how to make your tests pass and get to the green bar as soon as possible.
Fake it (‘Til You Make It). The idea with faking it is to write the simples implementation that will make your current tests pass, even if it’s as dumb as return a constant variable. There are two advantages to this seemingly contradictory approach: psychological (you get a satisfactory feeling seeing a green bar fast), and scope control (instead of trying to overcomplicate things thinking of 20 steps ahead you’re working on only what’s needed now, quite often that’s actually the only thing you need).
Triangulate is based on the previous rule (fake it) which essentially means that you write only the necessary implementation that satisfies your tests, no more, no less.
Obvious implementation is something that quite often we tend to implement right of the bat, which is good. But it demands a certain kind of clairvoyance on your part which might play out well or might not. Use triangulation when it does not.
One to many. When you work with collections start implementing operations that work without the collection or work an empty collection first. It goes back to Fake It rule.
These section describes tips and tricks working with xUnit frameworks.
Assertion is the building block of any xUnit framework. It has to be decisively boolean, either it passes or not. Make your assertions as clear as possible and do not try to test private variables and methods. If you find yourself wanting to do that then you have a design problem rather a test problem.
Fixtures are common objects needed by several tests. Use
setUp method in xUnit to initialize those variables needed for tests to run. There are scenarios when you’d want to have several test cases and fixtures for the object you’re testing. Fixtures represented by test case classes in xUnit.
External fixture. User
tearDown method to cleanup after yourself when you run tests. It helps with Isolation rule.
Test method. There are 3 groups of hierarchy that organize xUnit - module, class, method. Method starts with
test and represents an individual test case. A class represents a fixture.
Exception test. To test for exceptions you should catch them and ignore. Fail your test only if the exception isn’t thrown.
Design patterns are useful and can be beneficial in regards of TDD in two scenarios: test writing and refactoring. This section is an overview of some of the design patterns and how they are commonly used in regards of TDD.
Command - represent the invocation of a computation as an object, not just a message.
Value Object - avoid aliasing problems by making objects whose values never change once created.
Null Object - represent the base case of a computation by an object.
Template Method - represent invariant sequence of computation with an abstract method intended to be specialized through inheritance.
Pluggable Object - represent variation by invoking another object with two or more implementation.
Pluggable Selector - avoid gratuitous subclasses by dynamically invoking different methods for different instances.
Factory Method - create an object by calling a method instead of a constructor. Imposter - introduce variation by introducing a new implementation of existing protocol.
Composite - represent the composition of the behavior of a list of objects with an object.
Collecting Parameter - pass around a parameter to be used to aggregate the results of a computation in many different objects
This section refers to refactoring technics from Martin Fowler’s Refactoring book and other sources that are employed in TDD.
Reconcile Differences. Refactoring is about removing duplication. It is often nerve-wracking to do. To make it easier for yourself take small steps.
Isolate Change. Instead of changing the entire thing you want to refactor at once try to move slower and isolate smaller things to refactor and change. Some possible ways to isolate change are Extract Method, Extract Object, and Method Object.
Migrate Data. How do you move from one representation? Temporary duplicate the data.
Extract Method. How do you make a long, complicated method easier to read? Turn a small part of it into a separate method and call the new method.
Inline Method. How do you simplify control flows that have become too twisted or scattered? Replace a method invocation with the method itself.
Extract Interface. How do you introduce a second implementation of operations? Create an interface containing the shared operations.
Move Method. How do you move a method to the place where it belongs? Add it to the class where it belongs, then invoke it.
Method Object. How do you represent a complicated method that requires several parameters and local variables? Make an object out of the method.
Add Parameter. How do you add a parameter to a method?
Method Parameter to Constructor Parameter. How do you move a parameter from a method or methods to the constructor?
This section of the book addresses practical day-to-day concerns and questions practicing TDD.
How large should your steps be? This is one of those questions that all of us have starting using TDD, how much ground should each test cover? The answer, as with a lot of things, it depends but starting with tiny little steps helps to get you used to it and give you confidence.
What don’t you have to test? That is another question that inevitably raises itself doing TDD. In short, to get you going, you should test: conditionals, loops, operations, polymorphism. Long answer is that you need to figure it out for yourself, what tests give you confidence and what tests are redundant.
How do you know if you have good tests? Here are some telltale signs that you have bad tests: longs setup code, setup duplication, long running tests, fragile tests.
How does TDD lead to frameworks? This is a very interesting question. TDD in a way encourages you to apply good design principles that in effect promote decoupled configured code that is encapsulated. Open/Closed principle helps you use a piece of software as is or extend it without altering it. This in essence leads you creating frameworks in your code that are responsible for certain piece of functionality are are used by other parts of code to achieve their own results.
How much feedback do you need? TDD’s view of testing is pragmatic. In TDD, the tests are a means to an end - the end being code in which we have great confidence. You’ll have to decide for yourself how many tests you write and how much confidence and feedback you need.
When you should delete tests? Never delete a test if it reduces your confidence in the behavior of the system. On the other hand if two tests are redundant with respect of each other then you should probably get rid of them unless they communicate different scenarios for a reader.
How do the programming language and environment influence TDD? They either facilitate TDD or discourage it. With systems, languages, and environments that support TDD and refactoring you tend to do more experiments and smaller refactoring and testing steps. With systems, languages, and environments that do not support TDD and refactoring well enough you tend to cover more ground with each test and change or forgo testing in the first place.
Can you test drive enormous systems? Yes, the amount of functionality in the system has no bearing on the effectiveness of TDD.
Can you drive development with application-level tests? ATDD (Application Test-Driven Development) a.k.a. Story Driven BDD development is doable but sometimes in practice is not feasible. TDD gives you the level of control to choose what kind of tests you want to do and when (unit or integration or application, etc.).
How do you switch to TDD midstream? You apply a “boy scout” principle, every time you touch a piece of code you make it better and in this case add tests to it before changing/refactoring.
Who is TDD intended for? It depends on the value system you have. If you value less defects, clean design, and a steady confident pace than it is for you.
How does TDD relate to patterns? In a sense TDD drives design because when you treat your objects as black boxes and specify in your tests what they should do but not how they should do it then at the refactoring stage you come to sometimes surprising but good design decisions.
Why does TDD work? TDD increased confidence in your code, gives you ability to finally relax as a developer, increased trust from teammates and customers because the code you ship has less bugs/defects, and it gives you a shorter feedback loop.
How does TDD relate to the practices of Extreme Programming? TDD and Extreme Programming share the following common practices and traits: pairing, working fresh, continuous integration, simple design, refactoring, continuous delivery
Test-Driven Development by Example is a great book that introduces TDD practices to people new to it and gives some tips and tricks to seasoned practitioners. I highly recommend you reading it! If you want to discuss TDD or have questions about this book feel free to leave a commend below or reach out to me directly on Twitter or via email.