TDD
TDD
Codemanship Limited
www.codemanship.com
All rights reserved
© Jason Gorman, 2016
The right of Jason Gorman to be identified as the author of this work has
been asserted in accordance with Section 77 of the Copyright, Designs
and Patents Act 1988. No part of this publication may be copied,
reproduced, stored in a retrieval system, or transmitted, in any form or
by any means without the prior permission of the publisher, nor be
otherwise circulated in any form of binding or cover other than that in
which it is published and without a similar condition being imposed on
the subsequent purchaser.
CODEMANSHIP | TDD |2
ABOUT THE AUTHOR
ABOUT CODEMANSHIP
Will Price
Mark Withall
Phil Proom
Jon Barber
Erik De Bonte
Antony Gorman
Ilya Agoshkov
François Renaud-Philippon
CODEMANSHIP | TDD |4
CONTENTS
1. Before We Begin .................................................................. 9
2. Why Do TDD? ......................................................................11
Building the Right Thing .......................................................11
Keeping the Design Simple ..................................................12
Producing Code That’s Easy To Change ................................12
Making Sure the Software Always Works .............................13
Sustaining the Pace of Development ...................................13
Reliability vs. Productivity ....................................................13
3. What is TDD? .......................................................................17
4. How To TDD ........................................................................18
5. The Golden Rule ..................................................................29
Test-Driven Design vs. Design-Driven Testing.......................31
6. Start With the Question.......................................................33
7. Test Your Tests ....................................................................37
Fluent Assertions .................................................................40
8. One Reason To Fail ..............................................................43
9. Tests Should Be Self-Explanatory .........................................46
10. Speaking the Customer’s Language .................................51
11. Triangulating ...................................................................55
Triangulation Patterns .........................................................61
Obvious Implementations & TDD “Gears”............................62
12. Refactoring .....................................................................64
13. Design Principles .............................................................81
Simple Design ......................................................................81
CODEMANSHIP | TDD |5
Tell, Don’t Ask ..................................................................... 83
Single Responsibility ............................................................ 87
Swappability & Dependency Injection ................................. 91
Client-Specific Interfaces ..................................................... 93
Polymorphism & Contract Testing ....................................... 97
14. Test Doubles ................................................................. 102
Stubs ................................................................................. 103
Mock Objects .................................................................... 107
Dummies........................................................................... 111
Whose Interface Is It Anyway?........................................... 113
Mocks vs. Stubs vs. Dummies ............................................ 114
15. Test-Driving Integration Code ........................................ 117
16. TDD With The Customer ................................................ 125
Specification By Example ................................................... 126
User Stories – Placeholders For Conversations .................. 128
Test Completeness & Test Scope ....................................... 130
The Tests We Didn’t Think Of ............................................ 131
Definition of “Done” .......................................................... 131
Getting To “Done” In Vertical Slices ................................... 132
Executable Specifications .................................................. 134
The Customer Cannot Be Replaced .................................... 137
17. Driving Design From Customer Tests ............................. 140
Start With a Failing Customer Test ..................................... 141
Identify The Work.............................................................. 142
Identify The Knowledge Needed To Do The Work .............. 142
Name The Worker ............................................................. 142
CODEMANSHIP | TDD |6
Test-driving Adding A Title To The Library ..........................143
Test-driving Adding A Default Loan Copy To The Title ........147
Test-driving Reward Points ................................................149
Test-driving Email Alerts ....................................................152
The “London School” of TDD .............................................160
Making Customer Tests Run Faster ....................................162
Are We “Done” yet? ..........................................................163
18. The Testing Pyramid ......................................................165
19. TDD & Continuous Integration .......................................170
Before We Commit: Update/Merge & Test Locally.............171
After We Commit: Wait For The TestS To Pass On A Build Server
..........................................................................................172
Making Builds Fast.............................................................174
TDD & Continuous Delivery ...............................................176
Feature Branching & Feature Toggles ................................177
20. TDD & Legacy Code .......................................................179
What Makes Code “Legacy”? .............................................180
Start By Identifying The Change Point(s) ............................181
Next, Identify Inflection Points ..........................................182
Introduce Tests. Any Kind Of Tests. ....................................183
Break The External Dependencies ......................................184
The “Boy Scout” Rule .........................................................190
21. Beyond Test-Driven Development .................................192
Data-driven & Property-Based Tests ..................................192
Critical Code ......................................................................195
Mutation Testing ...............................................................196
Test-Driving The “Untestable” ...........................................199
CODEMANSHIP | TDD |7
Non-Functional TDD .......................................................... 202
Clean Code & Continuous Inspection ................................. 205
22. Mastering TDD .............................................................. 213
Building Habits .................................................................. 214
Make TDD Your Default Behaviour .................................... 215
Under The Radar ............................................................... 216
Under-promise, Over-deliver ............................................. 216
It’s Easier To Apologise Than Get Permission ..................... 217
Practice, Practice, Practice! ............................................... 218
CODEMANSHIP | TDD |8
1. BEFORE WE BEGIN
Summary:
1. To learn TDD, you must do TDD
2. You can tackle the exercises in any OO language
3. You will need:
a. A unit testing tool, based on the xUnit pattern, that
supports parameterized tests
b. Automated refactoring menu in your code editor
c. Mock objects framework
d. Pencil & paper
Summary:
TDD helps us to build the right software
TDD helps to avoid building features we don’t need, and
making the design too complicated
Refactoring is a key part of TDD. It helps us to keep code
easy to change
The short cycles of TDD, together with fast-running
automated tests, help us to keep our software always
working
TDD helps us to deliver working software sooner, and for
longer
Along with the risk of leaving important features out of our design,
there’s also the risk of including features that aren’t needed at all.
In software (as well as kitchens), unneeded features and
unnecessary complexity add costs, both initial and ongoing.
TDD encourages us to write the simplest code possible to pass our
tests. If the test doesn’t specifically ask for it, you ain’t gonna’ need
it.
Software that doesn’t work has no value. While we’re editing the
code, the code isn’t working.
TDD breaks development down into small cycles. These micro-
iterations typically last just a few minutes, at the end of which we
have tested, working code that could be shipped if necessary.
We automate the tests so they can be run quickly. This way, after
each small change, we can re-test the software to make sure it still
works.
99% of
teams are
here
reliability
most reliable
software at
lowest cost
TDD can help get us into a “sweet spot” of the most reliable code
at the lowest cost in four ways:
Agreeing executable tests catches many requirements
misunderstandings before we’ve written any code. These
requirements “bugs” can cost hundreds of times more to
fix in user testing or production
TDD breaks coding down into “baby steps”, bringing more
focus to every line of code we write and highlighting more
errors that we might have missed taking bigger bites
TDD encourages us to keep our code simple, and simpler
code is less likely to be wrong
The automated tests TDD creates enable us to check for
new bugs we might have introduced immediately after
making a change
Studies done of teams adopting TDD have convincingly shown that,
on average, test-driven code is much more reliable, but doesn’t
cost any more – and in many cases, costs less – to deliver working
software.
CODEMANSHIP | TDD |15
TDD is arguably the first defect prevention technique to have
gained widespread adoption.
Write a
failing test
“Green light”
Refactor to Write the
make the simplest
next test code to pass
easier the test
Summary:
Start with the simplest failing test you can think of
Write the simplest code you can think of to pass the test
quickly
If no need to refactor, move on to the next failing test
Refactor your test code, too!
Parameterized tests are a useful way to consolidate similar
test methods
Leave in duplication when it makes tests easier to
understand
Aim for one test method for each distinct rule. Use the test
name to clearly convey the rule
Tests should read like a specification
Localise dependencies on the objects under test
In TDD, we’re done when we can’t think of any more tests
that should fail
TDD is a process of design discovery
Tests make changes safer and easier
We’ll start by writing a failing test. (I’m doing it in Java, with the
Junit testing framework.)
Try to think of the simplest test you could start with – the one that
would be easiest to pass.
public class FibonacciTests {
@Test
public void firstNumberInSequenceIsZero() {
assertEquals(0, new Fibonacci().getNumber(0));
}
Let’s write the simplest code that will pass the test:
public class Fibonacci {
Next, let’s look at the code and see if we need to refactor it to make
the next test easier.
At this point, it’s hard to see how we could make this code easier
to change.
So let’s move on to the next failing test.
@Test
public void firstNumberInSequenceIsZero() {
assertEquals(0, new Fibonacci().getNumber(0));
}
@Test
public void secondNumberInSequenceIsOne() {
assertEquals(1, new Fibonacci().getNumber(1));
}
Again, we write the simplest code that will pass both of these tests.
public class Fibonacci {
Now that we’re back on a green light, it’s time to think about
refactoring again.
The implementation code looks okay, but there’s some very
obvious duplication in the test code. (Remember: test code needs
to be easy to change, too!)
The most direct way we could eliminate this duplication would be
to turn these two very similar test methods into a single
parameterized test covering both cases.
The built-in mechanism in JUnit for writing parameterized tests is a
bit clunky, so I’m going to use JUnitParams
(github.com/Pragmatists/JUnitParams) to make life easier.
@Test
@Parameters({"0,0","1,1"})
public void firstTwoNumbersAreSameAsIndex(int index,
int expected) {
assertEquals(expected,
new Fibonacci().getNumber(index));
}
FAILING TEST #3
@RunWith(JUnitParamsRunner.class)
public class FibonacciTests {
@Test
@Parameters({"0,0","1,1"})
public void firstTwoNumbersAreSameAsIndex(int index,
int expected) {
assertEquals(expected,
new Fibonacci().getNumber(index));
}
@Test
public void thirdNumberInSequenceIsOne(){
assertEquals(1, new Fibonacci().getNumber(2));
}
@Test
@Parameters({"0,0","1,1"})
public void firstTwoNumbersAreSameAsIndex(int index,
int expected) {
assertEquals(expected, getFibonacciNumber(index));
}
@Test
public void thirdNumberInSequenceIsOne(){
assertEquals(1, getFibonacciNumber(2));
}
We find it’s generally a good idea to limit the knowledge our test
code has of the interfaces of the objects being tested.
Let’s move on to another failing test.
FAILING TEST #4
@Test
public void fourthNumberInSequenceIsTwo(){
assertEquals(2, getFibonacciNumber(3));
}
We discovered one rule for the first two numbers, and a second
rule for the next two.
CODEMANSHIP | TDD |23
Let’s refactor the test code to reflect that, with another
parameterized test.
@Test
@Parameters({"2,1", "3,2"})
public void thirdNumberOnIsIndexMinusOne(int index,
int expected){
assertEquals(expected, getFibonacciNumber(index));
}
But we’re not done yet. How do we know that? We know because
we can think of more failing test cases.
FAILING TEST #5
The fifth number obeys the same rule as the third and fourth, so
that extra test is duplication that doesn’t make the specification
any easier to understand. Let’s merge it into the parameterized test
for third and fourth, and rename the test method to more
accurately describe the rule.
@Test
@Parameters({"0,0","1,1"})
public void firstTwoNumbersAreSameAsIndex(
int index,
int expected) {
assertEquals(expected, getFibonacciNumber(index));
}
@Test
@Parameters({"2,1", "3,2", "5,5"})
public void thirdNumberOnIsSumOfPreviousTwo(int index,
int expected){
assertEquals(expected, getFibonacciNumber(index));
}
To finish up, let’s see if we can think of any more failing test cases.
FAILING TEST #6
Our tests now read like a specification for our Fibonacci calculator.
Just by looking at the names of the test methods, we can see there
are three distinct rules, and the names clearly convey what those
rules are.
We discovered this design by working through a sequence of
examples – failing tests – and doing the simplest things we could
think of to pass them.
The end result is a working Fibonacci calculator, with a suite of fast-
running automated tests that will help us if we need to change the
calculator later.
return sequence[index];
}
}
It’s much safer to make this change because we have a good set of
automated tests that will alert us straight away if we break the
software.
This is a very important thing to remember about TDD: it may seem
like overkill to take such baby steps and write so many tests for such
a simple problem. But we’ve learned that by far the greater cost in
software development is the cost of changing code later, and for
the extra up-front investment of TDD, we get a potentially much
larger pay-off.
EXERCISE #2
Test-drive some code that will calculate the total net value of items
in a shopping cart represented as a list of unit price and quantity –
e.g., {{10.0, 5}, {25.5, 2}}, with the following discounts applied:
1. If total gross value > £100, apply a 5% discount
2. If total gross value > £200, apply a 10% discount
Summary:
Don’t write source code until a test requires it
Reference new classes, methods, variables etc in your test
first, so the code won’t compile, and then fix it by declaring
them
Aim to have just one thing broken at a time if possible
Next, I write code that passes a variable called items into the – as
yet non-existent - constructor of ShoppingBasket. Again, Eclipse
tells me there’s no such variable, and prompts me to fix it by
declaring one.
EXERCISE #3
Summary:
Write the test assertion first and work backwards to the
set-up
Tests have 3 components – set-up, action & assertions
Starting with the assertion helps us to discover what set-up
we need
Functional tests have three components:
The set-up: arranges objects and test data for the test
The action: invokes the method or function being tested
The assertion(s): asks the questions that will tell us if the
action worked
Intuitively, we tend to write test code in that order. But that can
lead us into difficulties.
How do we know what set-up we need for the test? It’s not
uncommon, when we write tests in the Arrange->Act->Assert
order, to get to the assertion and realise we’ve written the wrong
set-up for the question we want to ask.
The test is all about the question, so in TDD we recommend you
start there and work backwards to the set-up you need to ask it.
This may take some getting used to, but – with practice – you’ll start
to feel comfortable doing it this way.
Let’s look at an example to illustrate how to work backwards from
assertions.
In this example, we’re test-driving some code to combine 2 1-
dimensional arrays into a single 2D array.
We start by writing the assertion:
CODEMANSHIP | TDD |33
Notice that our assertion references three local variables that
haven’t been declared yet. By writing the assertion first, we’ve
discovered what set-up our test will need.
Now, let’s work backwards to create the set-up.
@Test
public void twoEmptyArraysCombineToAnEmpty2DArray(){
ArrayCombiner combiner = new ArrayCombiner();
int[] array1 = new int[]{};
int[] array2 = new int[]{};
assertArrayEquals(new int[][]{},
combiner.combine(array1, array2));
}
}
EXERCISE #4
Writing the assertions first and working backwards to the set-up,
test-drive some code to calculate how much water will be needed
to fill the following:
1. A cube
2. A cylinder
3. A pyramid
Summary:
See the test assertion fail, so you know that if the result is
wrong, the test will catch that
Implement just enough to see the assertion fail
Test names should clearly convey what’s supposed to
happen, to help developers fix it when a test fails
How we write assertions can make a difference to how
helpful test failure messages are in identifying the cause
Expected exceptions and mock object expectations are
kinds of assertions
In order for our automated tests to give us good assurance that the
code’s working, they need to be good tests.
It’s important to check that, if the result we get is wrong, the test
will fail.
For this reason, it’s highly recommended that – before you write
the code to pass the test – you see the test fail for the right reason.
public class VideoLibraryTests {
@Test
public void donatedTitleIsAddedToTheLibrary() {
VideoTitle title = new VideoTitle();
VideoLibrary library = new VideoLibrary();
library.donate(title);
assertTrue(library.getTitles().contains(title));
}
}
}
}
Now we can see that the test does indeed fail if the donated title
isn’t in added to the library.
SIDENOTE
Assertions don’t just come in the assert…() variety. Expected exceptions, and mock
object expectations (which we’ll cover later), are also kinds of assertions. Make
sure you see them fail, too.
FLUENT ASSERTIONS
When this test fails, we get more information in the failure trace.
Summary:
Tests should ask a single question, so that:
o We can bring more focus to each design decision
o Get more feedback with each decision
o More easily debug when tests fail
o Test code is easier to understand
@Test
public void donatedTitlesAddedToLibrary() {
Library library = new Library();
VideoTitle title = new VideoTitle();
Member donor = new Member();
library.donate(title, donor);
assertTrue(library.contains(title));
assertEquals(1, title.getRentalCopyCount());
assertEquals(10, donor.getPriorityPoints());
}
}
In this example, our test asks three questions. We’ve made three
design decisions in a single step, and will have to do more to get it
the test to pass.
Think, too, about what will happen if this test fails. Which part of
the implementation is broken? Tests that ask too many questions
are harder to debug when things break.
CODEMANSHIP | TDD |43
Tests that ask too many questions bring less focus on each design
decision and less feedback as we go - with the inevitable impact on
code quality that we observe as feedback cycles get longer.
It’s better to tackle this example in three tests, each one asking a
specific question.
public class LibraryTests {
@Before
public void donateTitle() {
library = new Library();
title = new VideoTitle();
donor = new Member();
library.donate(title, donor);
}
@Test
public void donatedTitlesAddedToLibrary(){
assertTrue(library.contains(title));
}
@Test
public void donatedTitlesHaveOneDefaultRentalCopy(){
assertEquals(1, title.getRentalCopyCount());
}
@Test
public void donorsGetTenPriorityPoints(){
assertEquals(10, donor.getPriorityPoints());
}
}
How many reasons does this test have to fail? I can see nine: each
individual number in the sequence has to be calculated correctly,
and they have to be separated by commas.
This approach means taking big leaps instead of baby steps, making
multiple design decisions before getting any feedback.
Better to break it down, like:
@Test
public void firstNumberInSequenceIsZero() {
Fibonacci fibonacci = new Fibonacci();
assertEquals("0",
fibonacci.generateSequence(8).split(",")[0]);
}
Summary:
Choose names of test methods to clearly convey what the
test is
Use names for helper methods, objects, fields, constants
and variables that clearly convey their role in the tests
Use test fixture names that make it easy to find tests
Pick test data that highlights boundaries in the logic
Name literal values – using constants or variables – if it
makes their significance clearer
Some duplication in test code is fine when it makes the test
easier to understand
@Before
public void init() {
a1 = new BankAccount();
a2 = new BankAccount();
a1.credit(100);
}
@Test
public void transferTest1() {
doAction();
assertEquals(50, a1.getBalance(), 0);
}
@Test
public void transferTest2() {
doAction();
assertEquals(50, a2.getBalance(), 0);
}
At first glance, it’s not immediately obvious what these tests are
about. Poor choices of names for the test fixture, test methods,
fields and helper methods make it harder to see that we’re testing
a funds transfer between a payer bank account and a payee.
If we refactor this code, we can make the intent clearer. Let’s start
with the test method names.
@Test
public void transferCreditsAmountToPayee() {
doAction();
assertEquals(50, a2.getBalance(), 0);
}
Test method names should clearly convey what the test is. Not how
the test works, or what method or class is being test: what is the
test?
Don’t worry if you have to write a long, verbose test method name.
We’re not designing an API, and we’ll probably never need to write
code that calls our test methods. Think like a newspaper headline
writer.
Now, how about those fields, a1 and a2?
private BankAccount payer;
private BankAccount payee;
Try to name test objects and test data (fields, variables, constants)
so they convey the role that object plays in the test. Ask “What does
the customer/user call this?”
Now, how about that unhelpfully general helper method,
doAction()?
@Test
public void transferCreditsAmountToPayee() {
transferFunds(payer, payee, 50);
assertEquals(50, payee.getBalance(), 0);
}
And finally, Tests1 isn’t a very illuminating name for a test fixture.
When someone asks “Where are the tests for bank accounts?”, it
won’t be of much help in finding them. Let’s rename it to make it
obvious what these are the tests for.
public class BankAccountTests {
As well as naming, our choice of test data can also help to make
tests clearer.
@Test(expected=InsufficientFundsException.class)
public void cannotWithdrawMoreThanBalance() {
BankAccount account = new BankAccount();
account.credit(100);
account.debit(100.01);
}
@Test(expected=InsufficientFundsException.class)
public void cannotWithdrawMoreThanBalance() {
BankAccount account = new BankAccount();
account.credit(BALANCE);
account.debit(BALANCE + 0.01);
}
Naming literal values like this can sometimes help to clarify its
significance in the test.
Lastly, don’t forget that – although we should seek to remove
duplication from our test code - if it makes it easier to understand,
leave it in. Readability trumps reuse.
EXERCISE #6
Revisit the code you write for exercises 1-5, and see if you can make
the tests easier to understand by refactoring them.
If you can find someone to help, ask them to read your tests and
comment on anything that isn’t totally clear.
A great way to practice choosing test method names when you’re
pair programming is for one person to declare the test, and then let
the other person write the test code based only on the name.
Summary:
The key to communicating on a software project is to
establish a shared language
Use the customer’s language when choosing names in your
code
Requirements documents and acceptance tests are a good
source of inspiration
A tag cloud generator is a cheap way of building a visual
glossary of customer terms
@Test
public void allocateFlagsPlaceForUser() {
PlaceRepository placeRepository =
new PlaceRepository();
User user = new User();
Place place =
placeRepository.allocate("A", 1, user);
assertEquals(user, place.flaggedFor());
}
If I asked you what business domain this code comes from, could
you tell by looking at the code?
How about if we change some of the names?
@Test
public void seatIsReservedForPassenger() {
FlightSeating seating = new FlightSeating();
Passenger passenger = new Passenger();
SeatReservation reservation
= seating.reserve("A", 1, passenger);
assertEquals(passenger,
reservation.getPassenger());
}
Summary:
Triangulation allows us to discover the simplest design one
test case at a time
Like triangulating a position on a map, it works by choosing
2 or more data points and finding the simplest solution that
satisfies them
Taking baby steps brings more focus on each design
decision and leads to better test assurance
Starting with the simplest failing test we can think of, we
gradually generalise the design just enough with each new
test
It requires at least 2 tests to generalise to a pattern or rule
Use test names to document the patterns/rules as they
emerge
As we triangulate our design, we may notice patterns to the
way code generalises that can help guide us
Sometimes, the implementation to pass a test is obvious
and trivial, and we don’t need to triangulate
a b
L
@Test
public void fibonacciIsSumOfPreviousTwoNumbers() {
assertEquals(21, new Fibonacci().getNumber(8));
}
if(index < 2)
return index;
return getNumber(index-1) + getNumber(index-2);
}
}
@Test
public void firstNumberIsZero() {
assertEquals(0, new Fibonacci().getNumber(0));
}
}
And then did the simplest thing possible to pass just that test.
public class Fibonacci {
Then we picked the next simplest failing test we could think of.
What we’re looking for is patterns (or rules). It’s not possible to
spot a pattern or generalise to a rule from just one example. With
two or more examples, we can begin to generalise.
The simplest pattern that fits the first two tests is that the Fibonacci
number is the same as its index.
Notice how we documented the pattern using a parameterized test
that consolidated those two examples.
@Test
@Parameters({"0,0", "1,1"})
public void firstTwoNumbersAreSameAsIndex(int expected,
int index) {
assertEquals(expected,
new Fibonacci().getNumber(index));
}
And now it’s time to refactor our test code again to consolidate
these two examples of this new rule.
What’s our next failing test? Well, the fifth Fibonacci number has
an index of 4 and a value of 3, so our current code would actually
pass that test. But the sixth has an index and value both of 5, so
that would fail.
@Test
@Parameters({"1,2", "2,3", "5,5"})
public void thirdNumberOnIsIndexMinusOne(int expected,
int index) {
assertEquals(expected, getFibonacciNumber(index));
}
The simplest solution that will pass all these tests is, in fact:
public int getNumber(int index) {
if(index < 2)
return index;
return getNumber(index-1) + getNumber(index-2);
}
The resulting tests read like a specification for these three rules,
and provide good test assurance that the rules have been correctly
CODEMANSHIP | TDD |60
implemented. If we broke the code so that it breaks one of the
rules, there’s a very good chance at least one test will fail, giving us
a vital early warning and making it easier to pinpoint exactly what’s
gone wrong.
Of course, we “know” the general solution, because we thought
about it in advance. Thinking about designs in advance is a good
thing. I highly recommend it!
But, even though it’s a good idea to think ahead, it’s not such a good
idea to code ahead. A trivial example like the Fibonacci calculator
tests our discipline in not leaping ahead for general solutions and
speculating about what the best design will be.
With programming, the devil is in the detail. Triangulating brings
more focus to getting those details right. Start simple, take baby
steps, and generalise only when you see a pattern.
TRIANGULATION PATTERNS
Observant readers may have noticed that there are loose patterns
to the way we generalise our solutions as we triangulate.
To pass a single test, we might need to do nothing more
than return the result the tests expects as a literal value.
To pass two tests that expect two different results, we
might generalise that literal value to a variable (or a
parameter).
When that value is accessed by more than one method (so
our implementation has to remember it), a variable might
become a field.
When a variable can have multiple values at the same time,
it can become a collection.
When that collection is a sequence that follows a rule, it
can become a loop – or a lambda expression - that
generates the collection, applying the rule to every
element.
EXERCISE #8
Summary:
Refactoring is improving the internal design of software
without changing what it does
Refactorings are small, atomic code re-writes that preserve
behaviour
Many refactorings can be automated
Run the tests after every refactoring to check nothing’s
broken
Refactorings are well-defined and have names like
Rename, Extract Method, Extract Class and Inline
Pay special attention to code duplication, as it can reveal
useful abstractions
In TDD, designs emerge through triangulation and
refactoring
Keep refactoring until you’re happy leaving the code as it is
RENAME
To make its meaning clearer, we may wish to rename a class, a
method, a variable and other things that have names. When we
change the name of, say, a method, that change breaks all of the
EXTRACT METHOD
INLINE
Inlining replaces a reference to a thing with the thing itself. For
example, we could inline the local variable indexOfFibonacci,
because we don’t really need it anymore.
EXTRACT CLASS
Extract Class moves selected features of an existing class into their
own new class, and replaces them in the old code with an instance
of the new class.
My editor’s refactoring menu doesn’t have a proper automated
Extract Class, so we’re going to go a bit around the houses here to
make it happen. Many refactorings require us to perform a
sequence of smaller refactorings.
EXERCISE #9
Look through the code you wrote for earlier exercises in this book
for anything that you’re not 100% happy with – names you think
could be made clearer, methods that do more than one thing,
nested IF statements, and so on.
Refactor the code until your confident that it will be easy to
understand and easy to change.
Summary:
A Simple Design (in order of priority):
o Works (i.e., passes all the tests)
o Is easy to understand
o Has minimal duplication
o Is as simple as possible
Design classes that Tell, Don’t Ask, sharing as little internal
detail as possible
Give methods and classes a single responsibility, so they
offer more possibilities for combinations and reuse
Compose objects from the outside, using dependency
injection, to offer greater flexibility for design and testing
Expose client-specific interfaces to hide methods that
client code doesn’t need to use
Use contract tests to ensure different implementations of
the same abstraction fulfil the contract of their super-type
SIMPLE DESIGN
double premiumPercent = 0;
premiumPercent +=
calculateAgePremium(
calculateAge(motorist.getDateOfBirth()));
premiumPercent +=
calculateGenderPremium(motorist.getGender());
int yearsOfExperience =
calculateExperience(license.getDateIssued());
premiumPercent +=
calculateExperiencePremium(yearsOfExperience);
premiumPercent +=
calculatePointsPremium(license.getPoints());
calculatePremium(carValue)
getLicense()
getDateOfBirth()
getGender()
getDateIssued()
getPoints()
calculatePremium(carValue)
calculatePremium(carValue)
calculatePremium(carValue)
Just at a glance, we can see there are far fewer object couplings.
Note that, because we’re sharing less data, there’s no need for all
those getter methods any more.
This design principle goes by several names, including data hiding
and encapsulation. All you need to remember is that the less
objects know about each other, the better.
SINGLE RESPONSIBILITY
While we’re about it, should the account class be responsible for
creating the XML string? Again, it’s foreseeable wanting to change
the XML format independently of how the account works. So, that,
too, belongs in its own class.
public void credit(double amount){
updateBalance(amount);
String dateTime =
new DateTimeFormatter().formatCurrentDateTime();
AccountLogger.log(
new XmlSerializer().serialize(this,
amount,
dateTime));
rewardPoints += calculateRewardPoints(amount);
}
CLIENT-SPECIFIC INTERFACES
The less objects in our software know about each other, the better.
As well as hiding internal features by applying Tell, Don’t Ask, we
also need to hide external features that our classes don’t need to
use.
To illustrate, look at this code from a community video library.
public Library(){
titles = new ArrayList<>();
}
Both Library and VideoStats use VideoTitle, but they use different
methods of it. Library just needs to know the name of the title,
while VideoStats just needs to access its ratings.
If we decide to change the details of either of these methods of
VideoTitle, then both clients will be affected.
CODEMANSHIP | TDD |94
We can hide methods that clients don’t need to see by splitting up
the interface, creating client-specific interfaces for Library and
VideoStats that only expose the methods they need.
public class VideoTitle implements Named, Rated {
@Override
public String getName() {
return name;
}
@Override
public List<Rating> getRatings() {
return ratings;
}
Note that the names of these new interfaces reflect the role the
objects play with respect to each client. These are not the names
of “things”, like Library and VideoTitle.
Now we can refactor Library and VideoStats so they depend only
on the interfaces they require.
public Library(){
titles = new ArrayList<>();
}
@Override
public int[] sortAsc(int[] input) {
boolean sorted = false;
while(!sorted){
sorted = true;
for (int i = 0; i < input.length - 1; i++) {
if(input[i] > input[i+1]){
swap(input, i, i+1);
sorted = false;
}
}
}
return input;
}
}
public class InsertionSort extends Sort {
@Override
public int[] sortAsc(int[] input) {
for (int i = 0; i < input.length - 1; i++) {
for(int j = i+1;j > 0;j--){
if(input[j] < input[j-1]){
swap(input, j, j-1);
}
}
}
return input;
}
}
@Test
@Parameters(method="data")
public void arrayIsSortedInAscendingOrder(int[] input) {
int[] output = createSort().sortAsc(input);
assertThat(Arrays.asList(output),
containsInAnyOrder(input));
for (int i = 0; i < output.length - 1; i++) {
assertThat(output[i],
is(lessThanOrEqualTo(output[i + 1])));
}
}
@Override
protected Sort createSort() {
return new BubbleSort();
}
}
public class InsertionSortTests extends SortTests {
@Override
protected Sort createSort() {
return new InsertionSort();
}
}
Summary:
Test doubles are objects used in tests that aren’t the real
thing
They can help us write fast-running tests by decoupling
from external dependencies like databases and web
services
They can help us defer implementation details by “faking it
‘til we make it”
They can help make tests that depend on changing or
random data repeatable
Stubs are test doubles that provide test data
Mocks are test doubles that allow us to test object
interactions, and help us to design objects that Tell, Don’t
Ask
Over-reliance on mock object frameworks can “bake in” a
tightly-coupled design
Dummies are test doubles that allow the test to compile
and run, but aren’t used
Test doubles should implement interfaces that we control,
to protect our application code from external
dependencies
Whether a test double is a stub, a mock or a dummy
depends on how it’s used, not how it’s implemented
There are often times, when we’re writing automated tests, that
we need to use an object that – for a number of possible reasons –
is not the real thing.
It could be:
STUBS
@Test
public void tradePriceIsStockPriceTimesQuantity() {
StockPricer pricer = new StockPricerStub(10);
TradeQuote trade = new TradeQuote(pricer);
assertEquals(1000, trade.quote(“X” , 100), 0);
}
}
In this test, we want to check that a quote for a stock market trade
is calculated correctly. Our TradeQuote object will get a price from
a StockPricer. When the software is in production, an
implementation of the StockPricer interface would connect to an
external web service. For the purposes of our test, though, we write
our own test-specific implementation that returns a price of 10.
Note the use of dependency injection here to plug the StockPricer
stub into the TradeQuote object (this is a great illustration of the
kind of flexibility we get by composing objects from the outside).
Internally, TradeQuote depends only on the interface, and knows
nothing about the stub.
public class TradeQuote {
Notice also how I passed the test data value into the constructor of
my stub, rather than hardcoding it into the stub’s implementation.
I’ve done this for two reasons; firstly, it means I can specify the
value in the actual test code, making it easier to understand.
Secondly, I can reuse this stub implementation with different
values, meaning less code duplication.
@Override
public double getPrice(String stock) {
return price;
}
}
@Override
public double getPrice(String stock)
throws StockNotFoundException {
throw new StockNotFoundException(stock);
}
}
In both tests, I used a stock symbol “X”. It doesn’t matter what stock
symbol we use, as our stubs will return the data we want them to
regardless.
Two important things to remember when using stubs:
1. Do not test the stub! Our goals here is to test the object
that uses the data the stub provides
CODEMANSHIP | TDD |105
2. Stubs are test code
Stubs can also be used to fix test data that would usually change
when using the real object - like a person’s age – making the test
repeatable.
@Test
public void driverUnder25PaysFivePercentPremium() {
Motorist motorist = new Motorist("01/01/1900",
Gender.MALE,
null,
new AgeCalculatorStub(24));
assertEquals(0.05, motorist.calculateAgePremium(), 0);
}
MOCK OBJECTS
Mocks often get mixed up with stubs (and it doesn’t help that many
developers use mock object frameworks to create stubs). The
terms are routinely used interchangeably, even by renowned
experts in TDD.
But, strictly speaking, a mock isn’t a stub. The purpose of a stub is
to provide test data. The purpose of a mock is to allow us to write
tests that will fail when an interaction between our object under
CODEMANSHIP | TDD |107
test and one of its collaborators doesn’t happen in the way we say
it should.
@Test
public void tellsAuditToLogQuote() throws Exception {
int quantity = 100;
String stock = "X";
StockPricer pricer = new StockPricerStub(10);
Audit audit = mock(Audit.class);
double quotedPrice =
new TradeQuote(pricer, audit)
.quote(stock, quantity);
verify(audit).log(stock , quantity , quotedPrice);
}
calculatePremium(carValue)
getLicense()
getDateOfBirth()
getGender()
getDateIssued()
getPoints()
Then things can get a bit sticky in our test code. The problem is that
mocking frameworks expose internal details about which methods
should get called. Just as surely as lots of getters break object
encapsulation, so too does lots of mocking code.
If we wanted to refactor this design to make it more loosely
coupled:
calculatePremium(carValue)
calculatePremium(carValue)
calculatePremium(carValue)
DUMMIES
Blink and you might have missed the fact that we already used
dummy objects in some of the tests in this chapter.
A dummy is an object that won’t be used in our test – of, if it is used,
we don’t care about it – but that has to be included so that we can
compile and run the test.
@Test
public void driverUnder25PaysFivePercentPremium() {
Motorist motorist = new Motorist("01/01/1900",
Gender.MALE,
null,
new AgeCalculatorStub(24));
assertEquals(0.05,
motorist.calculateAgePremium(), 0);
}
In this test, notice how we pass in a null value for license to the
Motorist constructor. We have to pass in something, or the test
code won’t compile. But this test doesn’t involve a DriversLicense,
so null is the simplest thing we can use.
It might be that the code we’re testing calls methods on a dummy
– but those methods don’t return any data (so we don’t need to
CODEMANSHIP | TDD |111
use a stub) – in which case we can use the Null Object design
pattern.
A Null Object is an empty implementation of an interface that we
can call methods on, but those methods don’t do anything.
A Null Object implementation for DriversLicense would require a
pure interface, with a dummy implementation that looks like this:
public interface License {
@Override
public void addPoints(int points) {
Imagine, in our example, that our external stock price provider has
created a convenient Java API for using their service.
public interface AcmeStocks {
@Test
public void tellsTitleToRegisterCopy() {
registerCopyInvoked = false;
Member member = new Member(){
public void awardPriorityPoints(int points){}
};
Title title = new Title(){
public void registerCopy(){
registerCopyInvoked = true;
}
};
new Library().donate(title, member);
assertTrue("title.registerCopy() was not invoked",
registerCopyInvoked);
}
@Test
public void tellsMemberToAwardTenPriorityPoints() {
awardPriorityPointsInvoked = false;
Member member = new Member(){
public void awardPriorityPoints(int points){
awardPriorityPointsInvoked = (points == 10);
}
};
Title title = new Title(){ public void registerCopy(){}};
new Library().donate(title, member);
assertTrue(
"member.awardPriorityPoints(10) was not invoked",
awardPriorityPointsInvoked);
}
Summary:
Minimise code that needs to be integration tested, so you
have to live with as few slow-running tests as possible
Aim for < 5% integration code (and <5% integration tests)
Isolate and minimise duplication of code that has external
dependencies
Use dependency injection to make integration code easily
swappable
Group fast-running and slow-running tests separately, so
we can easily choose which kind to run
For ultimate flexibility, package integration code separately
@Test
public void reviewsTestServiceHasTwoReviewsOfJaws3D() {
ReviewsService service =
new JSONReviewsService(
"http://localhost:8080/rottenpotatoes/json/reviews/");
Review[] reviews = service.fetchReviews("Jaws 3D");
assertEquals(2, reviews .length);
}
When we run this test, it will connect to a test reviews server at the
URL specified and use an HTTP GET to retrieve all reviews for Jaws
3D (of which we know there are two, because we control that test
data.)
In our implementation, a bunch of stuff happens:
@Override
public Review[] fetchReviews(String titleName) {
String json = "";
try {
url += URLEncode.encode(titleName, “UTF-8”) + “/get”;
CloseableHttpClient httpClient =
HttpClients.createDefault();
BufferedReader br =
new BufferedReader(new InputStreamReader(
(response.getEntity().getContent())));
String output;
httpClient.close();
@Override
public Review[] fetchReviews(String titleName) {
String json = client.get();
@Test
public void returnsDataFromSpecifiedRESTurl() {
String url = "http://localhost:8080/resttest/json/test";
RESTClient client = new RESTClient(url);
assertEquals("[{ foo : 0 }]", client.get("foo"));
}
}
We can reuse RESTClient for other services. Say, for example, we’re
asked to pull a release schedule of new video titles from an online
retailer’s REST API.
We can even go a step further, and package our integration code
(and associated tests) separately, so it can be reused in other
development projects. (NB: in this context, “package” means a unit
of release, like a Java JAR file, or a DLL in .NET.)
JSONReviewsService
REST
RESTClient
EXERCISE #12
Continuing with the same code you write for Exercise #11 (“Test-
drive some code that compares prices on TVs from three different
sources”), rig up test versions of those 3 data sources (a web
service, a simple TCP/IP daemon, and an Excel spreadsheet). Set-up
a local file to store audit logs.
Test-drive implementations that will get data from or write data to
these external sources. Try as much as possible to isolate the
external dependencies and minimise the code that really needs to
be integration tested.
Summary:
Examples help us to pin down the precise meaning of
requirements
We can extract data from customer examples to use in
tests
A user story is a placeholder to have a conversation with
the customer where we agree tests that will act as our
requirements specification
Writing tests is a skilled job, and the customer will probably
require our assistance to produce effective tests
The customer’s tests must define every input scenario the
software will need to handle
Negotiate feature scope and complexity by negotiating
tests
If you realise test cases have been missed, go back to the
customer to agree new tests. You are not the customer
A feature isn’t “done” until it passes the customer’s tests
Work in vertical slices, delivering working software that
passes the customer’s tests
Making customer tests machine-executable guarantees
absolute precision
Tools like FitNesse allow customers to provide test data we
can use in executable specifications
Once we have a failing customer test, we can implement a
design that will pass the test
Close customer involvement is vital. There’s no
workaround or substitute that works anywhere near as
well.
SPECIFICATION BY EXAMPLE
“hot”
“sweet”
“hot” = 90°C
Writing good tests for a user story can require a considerable time
investment from everyone involved, and this can encourage teams
to rush the process. When we miss test cases that our code will
need to handle, we end up with an incomplete specification, and –
ultimately – incomplete software.
The software must meaningfully handle every input that its
interface allows, so we’ll need at least one test to cover every
unique possibility.
If a user story generates too many tests, then that is a sign that it’s
too complicated. We can break complex stories down into sub-
requirements, as well as limiting test cases by simplifying or
constraining the allowable inputs.
For example, we could split “Donate a DVD” into “Donate a single
copy of a DVD” and “Donate multiple copies of a DVD”. Or we could
decide that users can only donate one copy at a time (since it will
probably be a rare occurrence for them to own multiple copies of
the same movie title.)
What we must never do is allow an input that the software doesn’t
handle. For example, if the library’s user interface allows members
to donate more than one copy, but the code only registers one
copy.
Try as we might to identify every test case for a user story before
we start writing code, the maxim “the map is not the terrain” will
inevitably apply.
While test-driving an implementation of our movie title class, we
might discover that it’s possible for there to be two different
movies with the same name. (For example, there are two movies
called “The Thing”.) How do we disambiguate them in the library?
We could identify movies by both the name and the year of release
(e.g., “The Thing (1982)” and “The Thing (2011)”).
But this is not a change we can make without rethinking our user
interface. As developers, we must be aware that every line of code
we write in some way defines the user’s experience.
If a change to the code will mean a change to the externally visible
or measurable functioning of the software, then we shouldn’t make
that decision by ourselves. It’s really a decision for the customer.
When you hit new test cases during implementation, take them to
the customer and specify the changes with them as part of their
tests for that feature.
DEFINITION OF “DONE”
EXECUTABLE SPECIFICATIONS
Summary:
The design process starts with a failing customer test
Identify the work the software has to do to pass the test
Identify what data is needed to do each piece of work
Assign responsibility for doing the work to objects that will
own the needed data (remember Tell, Don’t Ask)
Choose meaningful names for those objects, drawing
inspiration directly from the customer’s test
For each unique assertion in the customer’s test, test-drive
an implementation that will make that part “go green”
using the techniques we’ve explored in this book so far
Wire each worker object into the automated customer test
fixture, and see it pass before moving on to the next
assertion
Keep running the customer test. Feedback, feedback,
feedback!
Use test doubles (stubs, mocks, dummies) to exclude
external dependencies from the automated customer test,
and to allow us to “fake it ‘til we make it” for any
components we don’t want to implement yet
When the whole customer test is passing, consider
speeding up execution by adapting the test fixture to also
run as an xUnit test, if possible
Just because we’ve passed the automated customer test,
that doesn’t mean we’re “done” yet
For the user story “Donate a DVD”, we agreed a test with our
customer for the happy path, which we captured on a FitNesse Wiki
page, and made it into an executable specification with an empty
Java test fixture.
The first outcome we need to satisfy is that, after it’s been donated,
the library should contain that title.
So, the first piece of work our implementation needs to do is add
the donated title to the list of available titles in the library.
First, observing the Golden Rule, we write a failing unit test for
Library.
@Test
public void donatedTitlesAreAddedToAvailableTitles(){
Library library = new Library();
Title title = mock(Title.class);
library.donate(title);
assertTrue(library.getAvailableTitles()
.contains(title));
}
}
Notice how I’ve used Mockito to create a dummy Title for this test.
Title, at this point, needs no implementation, only an object
identity. No need to think about the implementation of Title while
we’re focusing on Library.
public class Title {
public Library() {
this.availableTitles = new ArrayList<>();
}
public Library() {
this.availableTitles = new ArrayList<>();
}
Now that we have Library doing the first piece of work, we should
wire it into the customer’s test so we can tick off that outcome.
@Test
public void defaultLoanCopyIsAdded(){
Title title = new Title();
title.addLoanCopy();
assertEquals(1, title.getCopyCount());
}
}
Now we can tick the second piece of work off in our customer test.
@Override
public void addLoanCopy() {
loanCopies++;
}
}
public Library() {
this.availableTitles = new ArrayList<>();
}
@Test
public void rewardingMemberAddsPointsToTotal() {
Member member = new Member();
member.reward(10);
assertEquals(10, member.getRewardPoints());
}
}
Library will need to tell the donor (Member) to reward itself with
10 points to pass the customer’s test.
@Override
public void reward(int points) {
this.rewardPoints += points;
}
The last piece of the jigsaw in our design for passing the customer’s
test is sending email alerts to members who expressed an interest
in matching titles.
There are four elements to this, so we’ll be doing it in four steps,
getting feedback with each.
1. Formatting the subject line of the email
2. Sending the email
3. Formatting the body of the email
4. Selecting the recipients
The last part will involve an external dependency. The plan will be
to do the first three pieces of work, and use a mock object to test-
driven the client-side code for the fourth part, all using fast-running
unit tests. Then we will test-drive an integration test for pushing
the email alert onto a queue, to be picked up and processed by an
external email server asynchronously.
@Test
public void subjectLineIncludesNewTitleName() {
EmailAlert alert =
new EmailAlert("The Abyss”);
assertEquals("Now available - The Abyss",
alert.getSubject());
}
}
Notice that this is the point where we implemented the name field
of Title, because this is the first outcome where it’s actually used.
We knew all along that this would be needed, but we only
implemented it when a test required it.
In TDD, it’s highly recommended that you think ahead about the
design, just as long as you don’t code ahead.
Before we can wire our new functionality into the customer’s test,
we’ll need to test-drive code to send the alert to an external
message queue.
As we don’t want our FitNesse test to actually send an email, we’ll
use a mock object for this external dependency, and test that a
method is called to push the alert onto the queue.
Before we move on, let’s get refactor away the duplicate set-up
code.
public class EmailAlertTests {
@Before
public void setupAlert() {
queue = mock(EmailQueue.class);
alert = new EmailAlert("The Abyss", queue);
}
@Test
public void subjectLineIncludesNewTitleName() {
assertEquals("Now available - The Abyss",
alert.getSubject());
}
@Test
public void sendingAlertPushesItOntoEmailQueue(){
alert.send();
verify(queue).send(alert);
}
Now we can wire this into the customer test, and use a mock
EmailQueue - at this point it’s just an interface – to capture the
subject value of the EmailAlert.
@Test
public void bodyIncludesNewTitleName() {
String expectedText = "Dear member, " +
"just to let you know that " +
"The Abyss is now available to borrow";
assertEquals(expectedText, alert.getBody());
}
Now we can write the code in our FitNesse fixture to make this part
of the customer test pass.
public String emailBody(){
return alert.getValue().getBody();
}
Next, let’s refactor the test code to consolidate these two very
similar tests into a single JUnitParams parameterised test.
@Test
@Parameters(method="recipientsParams")
public void recipientsListIsInterestedMemberEmails(
String[] memberEmails,
String recipients) {
InterestedMemberSearch search
= new InterestedMemberSearchStub(memberEmails);
EmailAlert emailAlert = new EmailAlert("X", null, search);
assertEquals(recipients, emailAlert .getRecipients());
}
This should handle any valid list of member emails. Now let’s wire
it into the FitNesse test.
public DonateFixture(){
search = new InterestedMemberSearchStub(
new String[]{
"[email protected]",
"[email protected]",
"[email protected]"
}
);
}
@Test
public void tellsDonatedTitleToAddLoanCopy(){
Copyable title = mock(Copyable.class);
Rewardable donor = mock(Rewardable.class); // dummy
Library library = new Library();
library.donate(title, donor);
verify(title).addLoanCopy();
}
}
In this failing test, we define that Library will communicate with the
donated title through a Copyable interface, which has an
addLoanCopy() method.
We don’t test that the donated title is added to the library here.
Instead, we test that the library invokes the addLoanCopy method.
The FitNesse test checks that the title was added to the library.
Once we have Library working to our satisfaction, we move inwards
to test-driving implementations of its collaborators, mocking their
collaborators, until our design’s working end to end, and we’re able
to pass the customer test completely.
public boolean libraryContains(){
return library.contains(title);
}
This is a reversal of the way we did things before, where we test-
CODEMANSHIP | TDD |161
driven the work the objects do with unit tests and wire them
together to pass the customer test. In this style of TDD – commonly
referred to as the “London school”, because that’s where it
originated from practitioners like Steve Freeman and Nat Pryce,
who wrote the book Growing Object Oriented Software Guided By
Tests – we drive the wiring using unit tests and let the customer
test check the work got done.
There are advantages and disadvantages to both approaches. The
first approach we saw duplicates testing of the work objects do, but
has the advantage of putting the tests closer to the modules being
tested, which can make debugging easier when tests fail, and
makes the tests themselves more portable. Also, an over-reliance
on mock objects can lead to issues with maintainability of the
design emerging is not modular enough.
The London School places greater emphasis on the object oriented
design and encourages a ‘Tell, Don’t Ask’ style of design, where we
focus on roles, responsibilities and especially collaborations
between objects.
On balance, we find that either approach can be successful. It’s
therefore a question of trying both and seeing which feels right for
you. Be sure, though: both approaches require considerable
practice to master.
Our customer test for donating a DVD takes almost a whole second
to run. If we have a couple of hundred such customer tests, we’d
CODEMANSHIP | TDD |162
have to wait 3 minutes to run the suite. In TDD, we seek the
shortest feedback loops possible, so can we speed this up?
One way to achieve that would be to adapt the test’s Java fixture
so it can also be run as a JUnit test.
@Test
public void donateMovieThatIsntInTheLibrary(){
setTitle("The Abyss");
setDonor("joepeters");
assertTrue(libraryContains());
assertEquals(1, copyCount());
assertEquals(10, rewardPoints());
assertEquals("Now available - The Abyss", emailSubject());
assertEquals("Dear member, just to let you know that “ +
“The Abyss is now available to borrow",
emailBody());
assertEquals("[email protected], “ +
“[email protected], “ +
“[email protected]",
recipients());
}
EXERCISE #15
Customer ~ 5-10%
Integration ~ 5-20%
~ 70-90%
Unit
EXERCISE #16
Summary:
The automated tests we create doing TDD can give us
confidence that our software is always working
Before committing code changes to a shared repository,
merge other people’s changes into your local copy and
make sure it passes all the tests
Run the tests as part of the build to catch problems caused
by local configurations
Use a “build token” to prevent issues caused by developers
committing conflicting changes on top of each other
Optimise test suite execution times to speed up builds
Create a build tree for large systems, where every
component has its own build process, and dependencies
are drawn from the outputs of successful sub-builds
Continuous Delivery is enabled by TDD
Avoid feature branching as a strategy for hiding unfinished
features from end users, because you will lose the benefits
of Continuous Integration
Use feature toggles to hide unfinished features until
they’re ready
shared
repository
It’s possible that the tests all passed on your local machine because
of some quirk of that machine’s configuration. (For example, there
may be a version of a library installed on your machine that other
machines don’t have.)
To help us eliminate this possibility, we wait to see that the code
builds and passes the tests on a different machine, often referred
to as a “build server”.
Most teams have a CI server like CruiseControl
(cruisecontrol.sourceforge.net) or Jenkins (jenkins.io) that “listens”
for new commits and triggers a build – complete with tests –
automatically.
Until the build succeeds on the build server, we don’t know for sure
that we haven’t broken the software. It’s highly advisable that
One team I know uses a felt beef burger as their build token. If a
team member plans to commit, they take the burger to their desk.
CODEMANSHIP | TDD |173
They only return it when their build has succeeded on the CI server.
Taking too long over commits has become known as “hogging the
burger”.
Some teams have automated tests suites that take a long time to
run. This slows Continuous Integration down, sometimes to a point
where there’s really nothing “continuous” about it.
A build – complete with automated testing – needs to take a few
minutes at the most. Firstly, this means we need to put significant
effort into optimising our test suites. If most of our tests drive the
software through the user interface, and/or include external
dependencies like reading and writing files, or using web services,
then it’s not uncommon for teams to have to wait more than an
hour to get feedback from the build server. This is an hour when it’s
not safe for anyone else to commit their changes.
Slow builds block teams.
Aim for a pyramid of tests: with mostly unit tests, fewer integration
tests, and just a handful of system tests. Some teams exclude the
bulk of their slow-running tests from the build process, falling back
on a handful of “smoke tests” that might catch any obvious
configuration problems. But this increases the risk of the build
testing missing more subtle conflicts in the logic. The more of your
tests you can run in the build, the lower that risk.
Instead of excluding slow-running tests, explore how they can be
speeded up – e.g., by caching data read from files, using in-memory
databases, or reusing datasets once they’re loaded so there’s less
need to set up multiple test databases, and so on.
B C
D E F G H
B C
D E F G H
In order to build and test A, we’ll have to build and test all of the
other components in the cycle (C & H).
EXERCISE #16
If you haven’t already, commit the code you created in the previous
two chapters into a shared online repository like GitHub.
Set up a Continuous Integration server – for example, using Jenkins
or CruiseControl – that will build the code and run all the
automated tests whenever changes are committed to that shared
repository.
Working with your customer, dream up one or two new user stories
that you believe will add value for any end users of the software.
Ask a friend or colleague – or your “customer”, if she is a developer
too – to share the work of implementing these new stories. Apply
the ideas covered in this chapter while you work together on
different parts of the code.
Use a feature toggle to hide the new functionality until it’s ready to
go.
Summary:
Legacy code is code for which we have no automated tests
The “Catch 22” with legacy code is that we need to refactor
to make automating tests easy, but it’s not safe to refactor
without automated tests
Start by identifying the “change point”: the part of the code
that will need to change to accommodate a new
requirement
Then identify “inflection points”: parts of the software that
directly depend on the change point, where – if we broke
the code – it would show
Introduce tests around the inflection points. These could
be unit tests, integration tests, system tests, or even
manual tests
Refactor the code to break the external dependencies
preventing us from making our tests fast-running
Keep running your inflection point tests after every
refactoring – no matter how long this takes. After
refactoring, it will get easier
Be a good “boy scout”, and leave code you work on in
better order than you found it to make the going easier in
future
But we can’t safely refactor the code because there are no tests.
It’s a chicken-and-egg situation.
int currentYear() {
return Calendar.getInstance().get(Calendar.YEAR);
}
}
Our customer also wishes to change the pricing logic so that movies
with IMDb ratings less than 4.0 get a $1 discount (they call it their
“bargain bin” price).
The calculatePrice() method of Pricer is our “change point” – the
part of the software we’ll need to change to accommodate the
customer’s new requirement.
: Rental
charge()
: Pricer
Connects to OMDb API
calculatePrice()
fetch()
Connects to MySQL DB
charge()
save(this)
Once we have some tests around the inflection points, the next
priority is to turn those into fast-running unit tests to make the
going easier.
Our goal is to test Rental without hitting the database or the IMDb
API, which means we need to make the classes that contain those
direct dependencies swappable.
Let’s start with the IMDb dependency, which exists in a static
method fetch() on the ImdbService class.
public class Pricer {
Now, we should run our inflection point tests. (Yes, even for a
change this small. Even if they’re manual tests.)
Rinse and repeat for RentalDAO, which can implement the same
DAO interface as CustomerDAO.
@Test
public void videosRatedLessThanFourOnImdbGetDollarOff() {
String imdbID = "tt2975590";
int customerID = 999;
Video video =
new Video(imdbID,
"Batman vs Superman",
2016,
3.9f);
Pricer pricer = new Pricer(createImdbStub(video),
createYearStub(2016));
Rental rental =
new Rental(imdbID,
customerID,
pricer ,
createCustomerDAOStub(customerID),
createRentalDAODummy());
rental.charge();
assertEquals(2.95,rental.getAmountCharged(),0.01);
}
Good scouts leave their campsites tidier than they found them.
Good developers leave code they change in better order than they
found it, too, to make future changes easier.
Notice there’s a fair amount of set-up code hidden away behind
helper methods in our test code. This is indicative of dependency
problems. Rental knows too much about other objects in the
system – it has too many collaborators.
A cleaner version might remove all the data access dependencies
from Rental and Pricer, so that they just handle the logic, and we
can handle fetching and saving data outside of that as a separate
concern (with its own set of tests.)
With fast-running tests to support us, this is a refactoring that will
be much easier and safer to do.
public class Rental {
EXERCISE #17
Find some legacy code that you could add value to. It could be code
you’ve worked on, or code from another project – maybe an Open
Source project – that uses technology you’re familiar with.
Build and run the software, and familiarise yourself with its
features.
Think about 1-3 small ways in which the software could be
improved by adding or changing features.
Apply the ideas covered in this chapter to test-drive the addition of
those changes, paying special attention to the discipline of re-
running the inflection point tests after ever refactoring.
Our final solution passes all of these tests, and – while we can’t
think of any more test cases that we’d expect to fail – maybe we
don’t have 100 confidence that our design will always work.
With a bit of extra work, we have the potential to test our code
against a much larger set of cases.
Notice how I renamed the test to more accurately describe the rule,
which is now explicitly described in the assertion. Generalising our
tests like this can make them more self-describing.
Now the expected result’s being calculated, we can generate as
much input data to drive this test as we like.
For example, we could generate a range of inputs from 0 to 100,
incrementing by 0.1 each time.
@Test
@Parameters(method="inputs")
public void squareOfSquareRootSameAsInput(double input){
double sqrt = Maths.sqrt(input);
assertEquals(input, sqrt * sqrt, 0.00001);
}
@Test
@Parameters(method="inputs")
public void squareOfSquareRootIsSameAsInput(double input) {
super.squareOfSquareRootIsSameAsInput(input);
}
I’ve extended the original test fixture, and added our test data
generator to this new subclass. When I want to run these
exhaustive tests, I run this test fixture. When I just want to run the
original unit tests, I run the original fixture. Easy as peas!
We can use any algorithm we like to generate our test data. It could
be a range, like we did here for this simple one-input problem.
If there are multiple input parameters that interact with each other
in our solution’s logic (e.g., “when a customer is over 18 AND has
more than 12 loyalty points THEN they get a free loan of any video
title”), then we could generate different combinations of inputs.
And we could generate random input values, just to see what
happens - a sort of automated exploratory testing.
@Test
@Configuration(tests=1000)
public void squareOfSquareRootIsSameAsInput(double input){
imply(input >= 0);
super.squareOfSquareRootIsSameAsInput(input);
}
}
CRITICAL CODE
Would we use techniques like this all the time, on all of our code?
Probably not. Though it varies from application to application, we
tend to find that parts of our code are more critical than others.
As a default, I recommend that you try to test-driven as much of
your code as possible. Not only does it lead to simpler, cleaner
designs, but the level of reliability basic TDD can achieve is good
enough for the majority of situations.
But some of your code is likely to be especially critical, and with that
code we may choose to go beyond TDD. Here are three factors you
may want to look out for when deciding how much extra testing
code might need:
1. Business risk – starting with customer tests, identify
scenarios where the stakes are high if the software breaks.
Talk to the customer and ask “how big a deal will it be if this
doesn’t work 1 time in 1,000? 1 time in 10,000? 1 time in 1
MUTATION TESTING
Although TDD can produce tests that give us confidence that our
code is working, there will always be times when we have doubts.
Just because the tests are all green, that doesn’t necessarily mean
our code has no bugs we need to worry about.
Experienced TDD practitioners recommend testing your tests by
running them to make sure they actually do fail when the code
gives the wrong result.
CODEMANSHIP | TDD |196
We can take this further with a technique called mutation testing.
Let’s ask ourselves how confident are we that if our square root
solution was broken in any way, one of our tests would catch it.
To do this, we can deliberately introduce an error, like changing a +
into a - .
do {
t = squareRoot;
squareRoot = (t + (number / t)) / 2;
} while ((t - squareRoot) != 0);
Then we run our unit tests to see if they “kill the mutant”.
The first test passes, but the second test gets stuck in an infinite
loop. None of these calculations should take more than a few
CODEMANSHIP | TDD |197
milliseconds. Let’s add a timeout to the tests, so we can see them
fail.
@Test(timeout=1000)
@Parameters({"0,0","1,1","4,2","0.25,0.5"})
public void squareOfSquareRootIsSameAsInput(
double input,
double expected) {
assertEquals(expected, Maths.sqrt(input), 0.00001);
}
The fact is, though, that we can test-driven pretty much anything
that we can automate in code, and we can automate pretty much
anything in any programming language. It just takes some ingenuity
and imagination.
Programming language choice is rarely the barrier it appears to be.
There’s a SQLUnit, a COBOLUnit, and even frameworks for unit
testing microprocessor designs. If it offers the ability to invoke
programs or functions written in that language, then we can test-
drive it.
SEPARATE CONCERNS
renders
Model View Model
forwards
events
GUI
Controller
The flow and logic of user interactions are handled by a core of plain
old objects with no direct external dependencies on a UI
framework, and therefore completely amenable to unit testing.
CODEMANSHIP | TDD |201
What’s left is a thin sliver of code that binds directly to the UI
framework’s API. In our testing pyramid, these bindings can be
checked using a much smaller number of system-level tests to
make sure everything’s wired together correctly.
FAKE IT
NON-FUNCTIONAL TDD
One of the risks when we write tests that, for example, specify a
maximum response time for a method, is that there may be times
when the computer is busy doing whatever it is that computers do
when we’re not looking.
The threading models of modern operating systems can be
unpredictable, and we might see a performance test very
occasionally fail.
This leads to what some programmers call “Heisenbugs” (a pun on
the discoverer of the Uncertainty Principle in quantum physics),
and flickering builds; builds that sometimes succeed and
sometimes fail.
Three ways to reduce this risk are:
a. Make sure you leave a good margin for error. If you
believe a method might take 10ms on average to complete,
setting the timeout for your test to 11ms is very likely to
produce flickering builds. Think “orders of magnitude”:
setting the timeout to 10x the average (100ms) should
make failures vanishingly rare. But not extinct.
Deploy Quality Gate – once we’re satisfied that our quality gate
works well enough to be tried out on real projects, we integrate it
into our build cycle. Before we do this, the team needs to agree on
the policy that they’ll stick to when this particular gate fails. A hard
gate will fail the build if any code is caught in the gate and the
developer who committed that code will have to address the issue.
CODEMANSHIP | TDD |209
A soft gate will report issues, but the team may have a process for
reviewing the suspect code and – if they feel it’s not a problem –
let it through.
Of course, like Jones the cat in the movie Alien, letting code quality
issues through the gate means they will show up again in later
inspections. If we’re happy to let code through, we need a
mechanism for ignoring previously addressed issues. There’s
nothing less useful than a continuous inspection report that’s
stuffed full of potential issues we already know about and chose to
ignore.
This is why many teams exclude previously raised issues from
inspection reports, so only new issues that get picked up are
highlighted. FxCop has a feature that enables this, but it can be a
bit hit and miss. In my implementation, I use an XML diff tool to
compare the new report with the previous one.
Clean Check-In – just as it’s a good idea to make sure the code
passes the functional tests before we commit it, we should also
check that it passes the code quality gates, so as not to risk breaking
the build and wasting the team’s time. I’m in the habit of running
code quality checks continuously on my desktop, typically only
going 10-15 minutes between inspections.
It helps enormously if you can run code analysis from within your
IDE. Visual Studio is quite advanced in this respect.
EXERCISE #19
EXERCISE #20
EXERCISE #21
By far the most commonly cited cause of failing with TDD is teams
who attempt to learn it while under delivery pressure. When
people are under pressure, we revert to our default behaviour.
At first, TDD feels clunky and difficult and slow. Developers who are
climbing the TDD learning curve see their productivity drop. Teams
UNDER-PROMISE, OVER-DELIVER
I’m going to finish this book where I started. To learn TDD, you need
to do TDD. Lots of TDD.
Use the exercises in this book. Seek out TDD project ideas on the
Web (searching for “TDD katas” is a good place to start). Come up
with your own ideas for projects you could test-drive.
And when you’ve completed an exercise or a project. Do it again,
better. I must have done the Fibonacci Numbers TDD kata 100
times, and I’m still discovering new ways of doing it.
Practice alone. Pair program with friends. Do a screencast of you
TDD-ing and watch it back (you’d be amazed the stuff we do that
we didn’t know we were doing). Grab a space, a laptop and
projector and “mob program”. Watch other developers doing TDD.
YouTube is crammed full of screencasts (though not all of them
setting great examples – but even TDD done badly can be
educational.)
Don’t be afraid to try. Don’t be afraid to fail.
Good luck!