Software Engineering, 2005 Test-Driven Development 1
Software Engineering
Test-Driven DevelopmentTest-Driven Development
Mira Balaban
Department of Computer Science
Ben-Gurion university
Based on: K. Beck: Test-Driven Development by Example.
And slides of: Grenning
Software Engineering, 2005 Test-Driven Development 2
What is Test-Driven Development?
An iterative technique for developing software.
As much about design as testing.• Encourages design from a user’s point of view.
• Encourages testing classes in isolation.
• Produces loosely-coupled, highly-cohesive systems
Grenning
Software Engineering, 2005 Test-Driven Development 3
What is TDD?
Must be learned and practiced.
• If it feels natural at first, you’re probably doing it wrong.
More productive than debug-later programming.
It’s an addiction rather than a discipline – Kent Beck
Grenning
Software Engineering, 2005 Test-Driven Development 4
TDD Mantra Red / Green / Refactor
1. Red – Write a little test that doesn’t work, and perhaps doesn’t even compile at first.
2. Green – Make the test work quickly, committing whatever sins necessary in the process.
3. Refactor – Eliminate all of the duplication created in merely getting the test to work.
Beck
Software Engineering, 2005 Test-Driven Development 5
TDD Development CycleStart
Refactor as needed
Write a test for a new capability
Run all teststo see them pass
Write the code
Run the testand see it fail
Fix compile errors
Compile
Grenning
Run all teststo see them pass
Software Engineering, 2005 Test-Driven Development 6
Do the Simplest Thing
Assume simplicity- Consider the simplest thing that could possibly work.
- Iterate to the needed solution.
When Coding:- Build the simplest possible code that will pass the tests.
- Refactor the code to have the simplest design possible.
- Eliminate duplication.
Grenning
Software Engineering, 2005 Test-Driven Development 7
TDD Process
Beck
Once a test passes, it is re-run with every change.
Broken tests are not tolerated.
Side-effects defects are detected immediately.
Assumptions are continually checked.
Automated tests provide a safety net that gives you the courage to refactor.
Software Engineering, 2005 Test-Driven Development 8
TDD Process What do you test? …
everything that could possibly break - Ron Jefferies
Don’t test anything that could not possibly break (always a judgment call)
Example: Simple accessors and mutators
Beck
Software Engineering, 2005 Test-Driven Development 9
TDD Process1. Start small or not at all (select one small piece of functionality
that you know is needed and you understand).
2. Ask “what set of tests, when passed, will demonstrate the presence of code we are confident fulfills this functionality correctly?”
3. Make a to-do list, keep it next to the computer
1. Lists tests that need to be written
2. Reminds you of what needs to be done
3. Keeps you focused
4. When you finish an item, cross it off
5. When you think of another test to write, add it to the list
Beck
Software Engineering, 2005 Test-Driven Development 10
Test Selection
Which test should you pick from the list? Pick a test that will teach you something and that you are confident you can implement.
Each test should represent one step toward your overall goal.
How should the running of tests affect one another? Not at all – isolated tests.
Beck
Software Engineering, 2005 Test-Driven Development 11
Assert First
Where should you start building a system? With the stories you want to be able to tell about the finished system.
Where should you start writing a bit of functionality? With the tests you want to pass on the finished code.
Where should you start writing a test? With the asserts that will pass when it is done.
Beck
Software Engineering, 2005 Test-Driven Development 12
TDD techniques:
Beck
Fake It (‘Til You Make It)
Triangulate
Obvious Implementation
Software Engineering, 2005 Test-Driven Development 13
After the first cycle you have:
1. Made a list of tests we knew we needed to have working.
2. Told a story with a snippet of code about how we wanted to view one operation.
3. Made the test compile with stubs.
4. Made the test run by committing horrible sins.
5. Gradually generalized the working code, replacing constants with variables.
6. Added items to our to-do list rather than addressing then all at once.Beck
Software Engineering, 2005 Test-Driven Development 14
“Expected surprises”: How each test can cover a small increment of
functionality.
How small and ugly the changes can be to make the new tests run.
How often the tests are run.
How many teensy-weensy steps make up the refactorings.
Software Engineering, 2005 Test-Driven Development 15
Motivating story
WyCash – A company that sold bond portfolio management systems in US dollars.
Requirement: Add multi-currencies.
Method used: Replace the former basic Dollar objects by Create Multi-Currency Money objects.• Instead of – revise all existing services.
Software Engineering, 2005 Test-Driven Development 16
Requirement – Create a report:Like this:
Instrument Shares Price Total IBM 1000 25 USD 25000 USD Novartis 400 150 CHF 60000 CHF
Total 65000 USD
Exchange rates: From To Rate
CHF USD 1.5
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10
Software Engineering, 2005 Test-Driven Development 17
The 1st to-do list:
We need to be able to add amounts in two different currencies and convert the result given a set of exchange rates.
We need to be able to multiply an amount (price per share) by a number (number of shares) and receive an amount.
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10
Software Engineering, 2005 Test-Driven Development 18
The First test:
We work on multiplication first, because it looks simpler. What object we need – wrong question! What test we need?
public void testMultiplication() {
Dollar five= new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
}
Software Engineering, 2005 Test-Driven Development 19
2nd to-do list:
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10Make "amount" privateDollar side-effects?Money rounding?
Test does not compile:1. No class Dollar.2. No constructor.3. No method times(int).4. No field amount.
Software Engineering, 2005 Test-Driven Development 20
Make the test compile:
class Dollar{
public int amount;
public Dollar(int amount){}
public void times(int multiplier){}
}
Software Engineering, 2005 Test-Driven Development 21
Test fails.
Is it bad? It’s great, we have something to work on!
Our programming problem has been transformed from "give me multi-currency" to "make this test work, and then make the rest of the tests work."
Much simpler.
Much smaller scope for fear.
We can make this test work.
Software Engineering, 2005 Test-Driven Development 22
Make it run -- Green bar:.The test:public void testMultiplication() {
Dollar five= new Dollar(5); five.times(2); assertEquals(10, five.amount);
}
class Dollar{public int amount = 10;public Dollar(int amount){}public void times(int multiplier){}
}
Software Engineering, 2005 Test-Driven Development 23
The refactor (generalize) step: Quickly add a test.
Run all tests and see the new one fail.
Make a little change.
Run all tests and see them all succeed.
Refactor to remove duplication between code and test –duplication is a source for dependencies between code and test, and dependency test cannot reasonably change independently of the code!
Software Engineering, 2005 Test-Driven Development 24
Remove Duplication:.
The test:public void testMultiplication() {
Dollar five= new Dollar(5); five.times(2); assertEquals(10, five.amount);
}
class Dollar{public int amount = 5 * 2;public Dollar(int amount){}public void times(int multiplier){}
}
Software Engineering, 2005 Test-Driven Development 25
Remove Duplication:class Dollar{
public int amount;public Dollar(int amount){}public void times(int multiplier){
amount = 5 * 2;}
}
Where did the 5 come from? The argument to the constructor:class Dollar{
public int amount;public Dollar(int amount){this.amount = amount}public void times(int multiplier){
amount = amount * 2;}
}
Software Engineering, 2005 Test-Driven Development 26
Remove duplication:The test:public void testMultiplication() {
Dollar five= new Dollar(5); five.times(2); assertEquals(10, five.amount);
}
The 2 is the value of the multiplier argument passed to times. Using *= removes duplication:class Dollar{
public int amount;public Dollar(int amount){this.amount = amount}public void times(int multiplier){
amount *= multiplier;}
}
The test is still green.
Software Engineering, 2005 Test-Driven Development 27
2nd to-do list:
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10Make "amount" privateDollar side-effects?Money rounding?
The current to-do list:
Software Engineering, 2005 Test-Driven Development 28
Our achievements:
Made a list of the tests we knew we needed to have working.
Told a story with a snippet of code about how we wanted to view one operation.
Made the test compile with stubs. Made the test run by committing horrible sins. Gradually generalized the working code, replacing
constants with variables. Added items to our to-do list rather than addressing
them all at once.
Software Engineering, 2005 Test-Driven Development 29
Our method: Write a test: Think about the operation as a story. Invent the interface
you wish you had. Include all elements necessary for the right answers.
Make it run: Quickly getting that bar to go to green dominates everything else. If the clean, simple solution is obvious but it will take you a minute, then make a note of it and get back to the main problem -- getting the bar green in seconds.
Quick green excuses all sins. But only for a moment. Make it right: Remove the duplication that you have introduced, and
get to green quickly.
The goal is clean code that works:Test-Driven Development: 1. “that works”, 2. “clean”.Model Driven Development: 1. “clean”, 2. “that
works”.
Software Engineering, 2005 Test-Driven Development 30
2nd to-do list:
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10Make "amount" privateDollar side-effects?Money rounding?
Next we’ll try to get rid of the side effects.
The current to-do list:
We’ll remove side effects:
Software Engineering, 2005 Test-Driven Development 31
Degenerate objects:
Write a new test for successive multiplications – that will test side effects:
public void testMultiplication() { Dollar five= new Dollar(5);five.times(2);assertEquals(10, five.amount);five.times(3); assertEquals(15, five.amount);
}
The test fails – red bar!
Decision: Change the interface of Dollar. Times() will return a new object.
Software Engineering, 2005 Test-Driven Development 32
Degenerate objects: Write another test for successive multiplications:
public void testMultiplication() { Dollar five= new Dollar(5); Dollar product= five.times(2); assertEquals(10, product.amount);product= five.times(3);assertEquals(15, product.amount);
}
The test does not compile!
Software Engineering, 2005 Test-Driven Development 33
Degenerate objects: Change Dollar.times() :
public Dollar times(int multiplier){amount *= multiplier;return null;
}
The test compiles but does not run!
public Dollar times(int multiplier){return new Dollar(amount * multiplier);
}
The test passes!
Software Engineering, 2005 Test-Driven Development 34
Achieved item in the to-do list:
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10Make "amount" privateDollar side-effects?Money rounding?
The current to-do list:
The “side-effects” item is achieved by turning Dollar.times() into a non-destructive (non-mutator) operation. This is the characterisation of Value Objects.
Software Engineering, 2005 Test-Driven Development 35
Our method in the last step:
We have translated a feeling -- disgust at side effects –
into a test -- multiply a Dollar object twice.
This is a common theme of TDD:
Translate aesthetic judgments into to-do items and into tests.
Translated a design objection (side effects) into a test case that failed because of the objection.
Got the code to compile quickly with a stub implementation.
Made the test work by typing in what seemed to be the right code.
Software Engineering, 2005 Test-Driven Development 36
Two strategies for test passing:
Fake It— Return a constant and gradually replace constants with variables until you have the real code.
Use Obvious Implementation— Type in the real implementation.
Software Engineering, 2005 Test-Driven Development 37
Equality for all – Value objects:
Dollar is now used as a value object.
The Value Object pattern constraints:
– they are not changed by operations that are applied on them.
– Values of instance variables are not changed.
– No aliasing problems (think of 2 $5 checks).
– All operations must return a new object.
– Value objects must implement equals() (all $5 objects are the same).
Software Engineering, 2005 Test-Driven Development 38
3rd to-do list:
If Dollar is the key to a hash table, then implementing equals() requires also an implementation of hashcode() (the equals – hash tables contract).
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10Make "amount" privateDollar side-effects?Money rounding?equals()hashcode()
Software Engineering, 2005 Test-Driven Development 39
Handling equals()Implementing equals()? – no no!
Writing a test for equality!
public void testEquality() { assertTrue(new Dollar(5).equals(new Dollar(5)));
}
The test fails (bar turns red).
Fake implementation in class Dollar: Just return true.public boolean equals(Object object) {
return true; }
Duplication: true is 5 == 5 which is amount == 5.
Software Engineering, 2005 Test-Driven Development 40
Triangulation generalization technique:
Triangulation is a generalization that is motivated by 2 examples or more.
Triangulation ignores duplication between test and model code.Technique: invent another example and extend the test:We add $5 != $6?
public void testEquality() { assertTrue(new Dollar(5).equals(new Dollar(5)));
assertFalse(new Dollar(5).equals(new Dollar(6))); }
Generalize Dollar.equality:public boolean equals(Object object) {
Dollar dollar= (Dollar) object; return amount == dollar.amount;
}
Software Engineering, 2005 Test-Driven Development 41
Triangulation considrations:
Triangulation could have been used in the generalization of Dollar.times().• Instead of following test-model duplications we could have
invented another multiplication example.
Beck’s suggestion: Use triangulation only if you do not know how to refactor using duplication removal.
Triangulation means trying some variation in a dimension of the design.
Software Engineering, 2005 Test-Driven Development 42
Review of handling the 4th to-do list:
Noticed that our design pattern (Value Object) implied an operation.
Tested for that operation.
Implemented it simply.
Didn't refactor immediately, but instead tested further.
Refactored to capture the two cases at once.
Software Engineering, 2005 Test-Driven Development 43
4th to-do list:
Equality requires also: Comparison with null. Comparison with other, non Dollar objects.
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10Make "amount" privateDollar side-effects?Money rounding?equals()hashcode()Equal nullEqual object
Software Engineering, 2005 Test-Driven Development 44
Privacy: 5th to-do list:
Equality of Dollar objects enables making the instance
variable amount private:
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10Make "amount" privateDollar side-effects?Money rounding?equals()hashcode()Equal nullEqual object
Software Engineering, 2005 Test-Driven Development 45
Improve the multiplication test: Turn the integers equality test into Dollar equality test:Replace:
public void testMultiplication() { Dollar five= new Dollar(5); Dollar product= five.times(2); assertEquals(10, product.amount);product= five.times(3);assertEquals(15, product.amount);
}
withpublic void testMultiplication() {
Dollar five= new Dollar(5); Dollar product= five.times(2); assertEquals(new Dollar(10), product);product= five.times(3);assertEquals(new Dollar(15), product);
}
Software Engineering, 2005 Test-Driven Development 46
Improve the multiplication test:
Get rid of the product variable:
public void testMultiplication() {
Dollar five= new Dollar(5);
assertEquals(new Dollar(10), five.times(2));
assertEquals(new Dollar(15), five.times(3));
}
This is a truly declarative test.
Now Dollar is the only class using the amount instance variable.
Therefore in the Dollar class:
Private int amount;
Software Engineering, 2005 Test-Driven Development 47
Achieved the 5th to-do list:
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10Make "amount" privateDollar side-effects?Money rounding?equals()hashcode()Equal nullEqual object
Software Engineering, 2005 Test-Driven Development 48
Achieved the 5th to-do list:
Note: The multiplication test is now based on equality! dependency among tests.If the equality test fails, then also multiplication fails!
The operations in the 5th to-do list step: Used functionality just developed to improve a test. Noticed that if two tests fail at once we're sunk. Proceeded in spite of the risk. Used new functionality in the object under test to reduce
coupling between the tests and the code (removal of the amount instance variable from the test).
Software Engineering, 2005 Test-Driven Development 49
Frank-ly speaking: the 6th to-do list:Observe the first test: It requires handling Francs. Seems TOO BIG for a “little step”. We insert a new object type Franc, that should pass the same test we
have for Dollar.
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10Make "amount" privateDollar side-effects?Money rounding?equals()hashcode()Equal nullEqual object5 CHF * 2 = 10 CHF
Software Engineering, 2005 Test-Driven Development 50
Franc multiplication test:
Copy the Dollar test:
public void testFrancMultiplication() {
Franc five= new Franc(5);
assertEquals(new Franc(10), five.times(2));
assertEquals(new Franc(15), five.times(3));
}
Software Engineering, 2005 Test-Driven Development 51
Pass the Franc multiplication test:
Recall the cycle steps:
Write a test. Make it compile. Run it to see that it fails. Make it run. Remove duplication.
We now have to accomplish step 4.We’ll copy the Dollar class! And create robust duplication!
Software Engineering, 2005 Test-Driven Development 52
Class Franc:
class Franc { private int amount; Franc(int amount) {
this.amount= amount; } Franc times(int multiplier) {
return new Franc(amount * multiplier); } public boolean equals(Object object) {
Franc franc= (Franc) object; return amount == franc.amount;
} }
Software Engineering, 2005 Test-Driven Development 53
Refactor – Following Franc Multiplication test:
We created robust duplication:
Classes Dollar and Franc.
Common equals.
Common times.
Too much for a single refactoring step.
We add these to our to-do list and move to the next cycle.
Software Engineering, 2005 Test-Driven Development 54
Equality for all: the 7th to-do list:
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10Make "amount" privateDollar side-effects?Money rounding?equals()hashcode()Equal nullEqual object5 CHF * 2 = 10 CHFDollar/Franc duplicationCommon equalsCommon times
Software Engineering, 2005 Test-Driven Development 55
Cleanup duplication: Insert a common superclass Money for the Dollar and Franc classes. We do it gradually, applying the tests all the time:
1. class Money All tests pass.
2. class Dollar extends Money { private int amount;
} All tests pass.
3. class Money { protected int amount;
} class Dollar extends Money { } All tests pass.
Software Engineering, 2005 Test-Driven Development 56
Work on equals:
4. Dollarpublic boolean equals(Object object) {
Money dollar = (Dollar) object; return amount == dollar.amount;
} All tests pass.
5. Dollarpublic boolean equals(Object object) {
Money dollar = (Money) object; return amount == dollar.amount;
} All tests pass.
6. Dollarpublic boolean equals(Object object) {
Money money = (Money) object; return amount == money. amount;
} All tests pass.
Software Engineering, 2005 Test-Driven Development 57
Move Dollar equals up:
7. Money
public boolean equals(Object object) { Money money = (Money) object; return amount == money. amount;
} All tests pass.
Software Engineering, 2005 Test-Driven Development 58
Eliminate Franc.equals():
We are going to work on Franc.equals(), but
there is no test for it!
Happens regularly:
Missing tests are revealed while refactoring.
A refactoring mistake is not be captured by the tests!
Write a test – extend the existing one, by duplicating the Dollar equality test:
public void testEquality() {
assertTrue(new Dollar(5).equals(new Dollar(5))); assertFalse(new Dollar(5).equals(new Dollar(6)));
assertTrue(new Franc(5).equals(new Franc(5))); assertFalse(new Franc(5).equals(new Franc(6)));
}
Software Engineering, 2005 Test-Driven Development 59
Eliminate Franc.equals():
8. class Franc extends Money { private int amount;
}
All tests pass.
9. class Franc extends Money { }
All tests pass.
Software Engineering, 2005 Test-Driven Development 60
10. Francpublic boolean equals(Object object) {
Money franc = (Franc object; return amount == franc.amount;
} All tests pass.
11. Francpublic boolean equals(Object object) {
Money franc = (Money) object; return amount == dollar.amount;
} All tests pass.
12. Francpublic boolean equals(Object object) {
Money money = (Money) object; return amount == money. amount;
} All tests pass.
Eliminate Franc.equals():
Software Engineering, 2005 Test-Driven Development 61
Now we can eliminate it. All tests are passed.
But – a new to-do entry: Compare Dollars with Francs.
The steps in this cycle: Stepwise moved common code from one class (Dollar) to
a superclass (Money).
Made a second class (Franc) a subclass also.
Reconciled two implementations—equals()—before eliminating the redundant one.
Eliminate Franc.equals():
Software Engineering, 2005 Test-Driven Development 62
Equality for all: the 8th to-do list:$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10Make "amount" privateDollar side-effects?Money rounding?equals()hashcode()Equal nullEqual object5 CHF * 2 = 10 CHFDollar/Franc duplicationCommon equalsCommon timesCompare Francs with Dollars
Software Engineering, 2005 Test-Driven Development 63
Comparing Dollars with Francs:
Add such a comparison:
public void testEquality() {
assertTrue(new Dollar(5).equals(new Dollar(5))); assertFalse(new Dollar(5).equals(new Dollar(6)));
assertTrue(new Franc(5).equals(new Franc(5))); assertFalse(new Franc(5).equals(new Franc(6)));
assertFalse(new Franc(5).equals(new Dollar(5)));
}
The test fails!
Software Engineering, 2005 Test-Driven Development 64
Comparing Dollars with Francs:
The test fails because equality does not check for being the same currency – Dollars ARE Francs!.
But there are no currency objects a new concept is needed. Can use Java same class comparison (smelly!).
Money
public boolean equals(Object object) { Money money = (Money) object; return amount == money. Amount && getClass().equals(money.getClass());
} All tests pass.
Software Engineering, 2005 Test-Driven Development 65
Comparing Dollars with Francs cycle:
Took an objection that was bothering us and turned it into a test.
Made the test run a reasonable, but not perfect way —getClass().
Decided not to introduce more design until we had a better motivation.
Software Engineering, 2005 Test-Driven Development 66
Making Objects: the 9th to-do list:$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10Make "amount" privateDollar side-effects?Money rounding?equals()hashcode()Equal nullEqual object5 CHF * 2 = 10 CHFDollar/Franc duplicationCommon equalsCommon timesCompare Francs with DollarsCurrency?
Software Engineering, 2005 Test-Driven Development 67
Get rid of the common times():
Similar implementations for times():
Franc:
Franc times(int multiplier) {
return new Franc(amount * multiplier);
}
Dollar:
Dollar times(int multiplier) {
return new Dollar(amount * multiplier);
}
Software Engineering, 2005 Test-Driven Development 68
Get rid of the common times():
1. Insert a common return type:
Franc:
Money times(int multiplier) {
return new Franc(amount * multiplier);
}
Dollar:
Money times(int multiplier) {
return new Dollar(amount * multiplier);
}
All tests pass!
Software Engineering, 2005 Test-Driven Development 69
Get rid of the common times():
2. Aim: eliminate the Money subclasses – not doing enough work to justify their existence.
Can do it only if there are no explicit references to their objects.
Method: • Insert factory methods for Dollar and for Franc in the
Money class.
• Replace the Dollar and France explicit references with that factory method.
Software Engineering, 2005 Test-Driven Development 70
A factory method:
public void testMultiplication() {
Dollar five= Money.dollar(5);
assertEquals(new Dollar(10), five.times(2));
assertEquals(new Dollar(15), five.times(3));
}
The factory method in Money:
static Dollar dollar(int amount) {
return new Dollar(amount);
}
Software Engineering, 2005 Test-Driven Development 71
Remove explicit references to Dollar:public void testMultiplication() {
Money five= Money.dollar(5); assertEquals(new Dollar(10), five.times(2));assertEquals(new Dollar(15), five.times(3));
}
Compilation problem: times is not defined for Money.There is no implementation for times in Money
Make Money abstract:
abstract class Money{abstract Money times(int multiplier); static Money dollar(int amount) {
return new Dollar(amount); }
} All tests pass!
Software Engineering, 2005 Test-Driven Development 72
Remove explicit references to Dollar:3. Use the factory method everywhere in the tests:public void testMultiplication() {
Money five= Money.dollar(5); assertEquals(Money.dollar(10), five.times(2));assertEquals(Money.dollar(15), five.times(3));
}
public void testEquality() { assertTrue(Money.dollar(5).equals(Money.dollar(5)));
assertFalse(Money.dollar(5).equals(Money.dollar(6))); assertTrue(new Franc(5).equals(new Franc(5)));
assertFalse(new Franc(5).equals(new Franc(6)));assertFalse(new Franc(5).equals(Money.dollar(5)));
} The tests are passed!
Software Engineering, 2005 Test-Driven Development 73
Remove explicit references to Dollar
Achieved: No client code knows that there is a subclass called
Dollar.
By decoupling the tests from the existence of the subclasses, we have given ourselves freedom to change inheritance without affecting any model code.
Software Engineering, 2005 Test-Driven Development 74
Remove explicit references to Franc:4. Use the factory method everywhere in the tests:public void testFrancMultiplication() {
Money five= Money.franc(5); assertEquals(Money.franc(10), five.times(2));assertEquals(Money.franc(15), five.times(3));
}
public void testEquality() { assertTrue(Money.dollar(5).equals(Money.dollar(5)));
assertFalse(Money.dollar(5).equals(Money.dollar(6))); assertTrue(Money.franc(5).equals(Money.franc(5)));
assertFalse(Money.franc(5).equals(Money.franc(6)));assertFalse(Money.franc(5).equals(Money.dollar(5)));
} The tests are passed!
Software Engineering, 2005 Test-Driven Development 75
The factory method for Franc:
The factory method in Money:
static Money franc(int amount) {
return new Franc(amount);
}
Note: The testFrancMultiplication() test maybe redundant –
Same like testMultiplication() for Dollar.
Software Engineering, 2005 Test-Driven Development 76
Achievements in this cycle:
Took a step toward eliminating duplication by reconciling the signatures of two variants of the same method—times().
Moved at least a declaration of the method to the common superclass.
Decoupled test code from the existence of concrete subclasses by introducing factory methods.
Noticed that when the subclasses disappear some tests will be redundant, but took no action.
Software Engineering, 2005 Test-Driven Development 77
Currency: the 10th to-do list:$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10 Delete testFrancMultiplication?Make "amount" privateDollar side-effects?Money rounding?equals()hashcode()Equal nullEqual object5 CHF * 2 = 10 CHFDollar/Franc duplicationCommon equalsCommon timesCompare Francs with DollarsCurrency?
Software Engineering, 2005 Test-Driven Development 78
Currency? How to implement currency? –
No, no. Wrong question! How to test currencies! In the future – complicated objects with flyweight
factories (to ensure controlled creation). Now – only strings:
public void testCurrency() {assertEquals("USD", Money.dollar(1).currency()); assertEquals("CHF", Money.franc(1).currency());
}
Software Engineering, 2005 Test-Driven Development 79
1. Insert a currency() method:
Money
abstract String currency();
Implement it in both subclasses:
Franc
String currency() { return "CHF"; }
Dollar
String currency() { return "USD"; }
All tests pass!
Software Engineering, 2005 Test-Driven Development 80
2. Refactoring: Achieving a common implementation for currency() in Dollar and Franc:
Francprivate String currency;Franc(int amount) {
this.amount = amount; currency = "CHF";
} String currency() {
return currency; }
Dollarprivate String currency;Dollar(int amount) {
this.amount = amount; currency = “USD";
} String currency() {
return currency; }
Software Engineering, 2005 Test-Driven Development 81
3. Refactoring: Push up the variable and the implementation of currency():
Money
protected String currency;
String currency() {
return currency;
}
Achieved: All tests pass!
Software Engineering, 2005 Test-Driven Development 82
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10 Delete testFrancMultiplication?Make "amount" privateDollar side-effects?Money rounding?equals()hashcode()Equal nullEqual object5 CHF * 2 = 10 CHFDollar/Franc duplicationCommon equalsCommon timesCompare Francs with DollarsCurrency?
Currency achieved!
Software Engineering, 2005 Test-Driven Development 83
Refactoring: Making the constructors of Dollar and Franc identical (1):
Add a parameter to the constructor – the idea is to remove the Explicit currency from the constructor:Franc
Franc(int amount, String currency) { this.amount = amount; this.currency = "CHF";
} This breaks the two callers of the constructor:Money
static Money franc(int amount) { return new Franc(amount, null); }
FrancMoney times(int multiplier) {
return new Franc(amount * multiplier, null); }
Software Engineering, 2005 Test-Driven Development 84
Intrrupt:Correct times() to call the factory method:
Franc
Money times(int multiplier) {
return Money.franc(amount * multiplier);
}
Interruption rules:
1. Entertain a brief interruption, but only a brief one.
2. Never interrupt an interruption (Jim Coplien)
Software Engineering, 2005 Test-Driven Development 85
Refactoring: Making the constructors of Dollar and Franc identical (2):
The factory method can pass "CHF":Money
static Money franc(int amount) { return new Franc(amount, "CHF");
} Assign the parameter to the instance variable:Franc
Franc(int amount, String currency) { this.amount = amount; this.currency = currency;
}
Software Engineering, 2005 Test-Driven Development 86
Refactoring: Making the constructors of Dollar and Franc identical (3):
Analogous change to Dollar in one “big” step:Money
static Money dollar(int amount) { return new Dollar(amount, “USD");
} Dollar
Dollar(int amount, String currency) { this.amount = amount; this.currency = currency;
} Money times(int multiplier) {
return Money.dollar(amount * multiplier); }
Software Engineering, 2005 Test-Driven Development 87
Refactoring: Push up the implementation of the constructors:
The 2 constructors are identical! Push up the implementation:
MoneyMoney(int amount, String currency) {
this.amount = amount; this.currency = currency;
} Dollar
Dollar(int amount, String currency) { super(amount, currency);
} Franc
Franc(int amount, String currency) { super(amount, currency);
}
Software Engineering, 2005 Test-Driven Development 88
Achievements in this cycle:
Were a little stuck on big design ideas, so we worked on something small we noticed earlier.
Reconciled the two constructors by moving the variation to the caller (the factory method).
Interrupted a refactoring for a little twist, using the factory method in times().
Repeated an analogous refactoring (doing to Dollar what we just did to Franc) in one big step.
Pushed up the identical constructors.
Software Engineering, 2005 Test-Driven Development 89
Interesting Times: the 11th to-do list:
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10 Delete testFrancMultiplication?Make "amount" privateDollar side-effects?Money rounding?equals()hashcode()Equal nullEqual object5 CHF * 2 = 10 CHFDollar/Franc duplicationCommon equalsCommon timesCompare Francs with DollarsCurrency?
Software Engineering, 2005 Test-Driven Development 90
Refactoring -- Interesting Times: Making Dollar.times() and Franc.times() identical:
The two implementations are still not identical:
Franc
Money times(int multiplier) {
return Money.franc(amount * multiplier);
}
Dollar
Money times(int multiplier) {
return Money.dollar(amount * multiplier);
}
Software Engineering, 2005 Test-Driven Development 91
1. Refactoring: Making Dollar.times() and Franc.times() identical:
Inline the factory methods:
Franc
Money times(int multiplier) {
return new Franc(amount * multiplier, “CHF”);
}
Dollar
Money times(int multiplier) {
return new Dollar(amount * multiplier, “USD”);
}
Software Engineering, 2005 Test-Driven Development 92
2. Refactoring: Making Dollar.times() and Franc.times() identical:
In Franc: the currency instance variable is always "CHF":Franc
Money times(int multiplier) { return new Franc(amount * multiplier, currency);
} In Dollar: the currency instance variable is always “USD":Dollar
Money times(int multiplier) { return new Dollar(amount * multiplier, currency);
}
Software Engineering, 2005 Test-Driven Development 93
3. Refactoring: Making Dollar.times() and Franc.times() identical:
Try to replace Franc with Money. Does it matter? -------- ask the tests!
FrancMoney times(int multiplier) {
return new Money(amount * multiplier, currency); }
Money cannot be abstract:Money
class Money Money times(int multiplier) {
return null; }
Software Engineering, 2005 Test-Driven Development 94
4. Refactoring: Making Dollar.times() and Franc.times() identical:
Red bar! Error message:
"expected:<Money.Franc@31aebf> but was: <Money.Money@478a43>".
Not as helpful.
Define toString() for a better error message:
public String toString() {
return amount + " " + currency;
}
Code without a test? Exception cause: Results on the screen. toString() is used only for debug output. the risk of it failing is low. Already have a red bar! Prefer not to write a test within a red bar.
Software Engineering, 2005 Test-Driven Development 95
5. Refactoring: Making Dollar.times() and Franc.times() identical:
New error message:"expected:<10 CHF> but was:<10 CHF>".
Right data but wrong class: Money instead of Franc. Problem results from testFrancMultiplcation(), that is based on the
implementation of equals():
public void testFrancMultiplication() { Money five= Money.franc(5); a MoneyassertEquals(Money.franc(10), five.times(2));assertEquals(Money.franc(15), five.times(3));
}
Moneypublic boolean equals(Object object) {
Money money = (Money) object; return amount == money.amount
&& getClass().equals(money.getClass()); }
Software Engineering, 2005 Test-Driven Development 96
6. Refactoring: Making Dollar.times() and Franc.times() identical:
Should check for same currencies not same classes.
A new requirement –
Assert Money(10, “CHF”), Franc(10, “CHF) as equal!
TDD Methodology write a test. TDD rule: Do not write a test on a red bar! RETREAT Green bar!
Franc
Money times(int multiplier) {
return new Franc(amount * multiplier, currency);
}
Software Engineering, 2005 Test-Driven Development 97
7. An “inner” cycle: Making Moneys and Francs equal:
A new test:public void testDifferentClassEquality() {
assertTrue(new Money(10, "CHF").equals(new Franc(10, "CHF")));
}
Red bar!
equals() compares currencies, not classes:
Money
public boolean equals(Object object) {
Money money = (Money) object;
return amount == money.amount
&& currency().equals(money.currency());
}
Software Engineering, 2005 Test-Driven Development 98
8. Refactoring: Making Dollar.times() and Franc.times() identical:
Return a Money object from Franc.times():Franc
Money times(int multiplier) { return new Money(amount * multiplier, currency);
} Same for Dollar:Dollar
Money times(int multiplier) { return new Money(amount * multiplier, currency);
} Identical implementations! push up!Money
Money times(int multiplier) { return new Money(amount * multiplier, currency);
}
Software Engineering, 2005 Test-Driven Development 99
Achievements in this cycle:
Reconciled two methods—times()—by first inlining the methods they called and then replacing constants with variables.
Wrote a toString() without a test just to help us debug.
Tried a change (returning Money instead of Franc) and let the tests tell us whether it worked.
Backed out an experiment and wrote another test. Making the test work made the experiment work.
Software Engineering, 2005 Test-Driven Development 100
Eliminate subclasses: the 12th to-do list:
$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10 Delete testFrancMultiplication?Make "amount" privateDollar side-effects?Money rounding?equals()hashcode()Equal nullEqual object5 CHF * 2 = 10 CHFDollar/Franc duplicationCommon equalsCommon timesCompare Francs with DollarsCurrency?
Software Engineering, 2005 Test-Driven Development 101
1. Refactoring: Eliminate the Dollar and Franc subclasses:
Dollar and Franc have only their constructors – which are identical.
No reason to have the subclasses.
Delete the subclasses.
Replace references to the subclasses with references to the superclass without changing the meaning of the code.
• Change the reference to Franc in the factory nethod:
static Money franc(int amount){
return new Money(amount, "CHF");
} Change the reference to Dollar in the factory method:
static Money dollar(int amount) {
return new Money(amount, "USD");
}
Software Engineering, 2005 Test-Driven Development 102
2. Refactoring: Eliminate the Dollar and Franc subclasses:
Dollar has no references can be removed!
Franc has a reference – in the equality test:
public void testDifferentClassEquality() {assertTrue(new Money(10, "CHF").equals(
new Franc(10, "CHF"))); }
Do we need this test?
Software Engineering, 2005 Test-Driven Development 103
3. Refactoring: Eliminate the Dollar and Franc subclasses:
Look at the other equality test:public void testEquality() { assertTrue(Money.dollar(5).equals(Money.dollar(5))); assertFalse(Money.dollar(5).equals(Money.dollar(6))); assertTrue(Money.franc(5).equals(Money.franc(5))); assertFalse(Money.franc(5).equals(Money.franc(6))); assertFalse(Money.franc(5).equals(Money.dollar(5))); }
Too detailed: (1) = (3); (2) = (4)! Simplify!
public void testEquality() { assertTrue(Money.dollar(5).equals(Money.dollar(5))); assertFalse(Money.dollar(5).equals(Money.dollar(6))); assertFalse(Money.franc(5).equals(Money.dollar(5))); }
==
Software Engineering, 2005 Test-Driven Development 104
4. Refactoring: Eliminate the Dollar and Franc subclasses:
The test testDifferentClassEquality() is needed only if there are
multiple classes.
But, we try to remove the subclasses.
The test can be removed! The Franc class is removed.
All tests are passed!
Multiplication tests:• There are separate tests for Dollar and for Franc.
• Same logic.
Remove testFrancMultiplication()
Software Engineering, 2005 Test-Driven Development 105
Achievements in this cycle:
Finished gutting subclasses and deleted them.
Eliminated tests that made sense with the old code structure but were redundant with the new code structure.
Software Engineering, 2005 Test-Driven Development 106
The 13th to-do list:$5 + 10 CHF = $10 if rate is 2:1$5 * 2 = $10 Delete testFrancMultiplication?Make "amount" privateDollar side-effects?Money rounding?equals()hashcode()Equal nullEqual object5 CHF * 2 = 10 CHFDollar/Franc duplicationCommon equalsCommon timesCompare Francs with DollarsCurrency?
Software Engineering, 2005 Test-Driven Development 107
Current Tests:public void testMultiplication() {
Money five= Money.dollar(5); assertEquals(Money.dollar(10), five.times(2));assertEquals(Money.dollar(15), five.times(3));
}
public void testEquality() { assertTrue(Money.dollar(5).equals(Money.dollar(5))); assertFalse(Money.dollar(5).equals(Money.dollar(6))); assertFalse(Money.franc(5).equals(Money.dollar(5)));
}
public void testCurrency() {assertEquals("USD", Money.dollar(1).currency()); assertEquals("CHF", Money.franc(1).currency());
}
Software Engineering, 2005 Test-Driven Development 108
Current Code:public class Money {
protected int amount;protected String currency;public Money(int amount, String currency) {
this.amount = amount; this.currency = currency;
}public Money times(int multiplier) {
return new Money(amount * multiplier, currency);; } public static Money dollar(int amount) {
return new Money(amount, “USD”); } public static Money franc(int amount) {
return new Money(amount, “CHF”); } public boolean equals(Object object) {
Money money = (Money) object; return amount == money. Amount
&& currency().equals(money.currency()); }public String currency() {
return currency;}public String toString() {
return amount + " " + currency; }
}
Software Engineering, 2005 Test-Driven Development 109
14. Addition – at last: A new to-do list:
$5 + 10 CHF = $10 if rate is 2:1$5 + $5 = $10
Remove achieved items. Handle simple items.
The new list:
$5 + 10 CHF = $10 if rate is 2:1
Start with a simpler addition:
Software Engineering, 2005 Test-Driven Development 110
Test for simple addition:Start with………. A Test:
public void testSimpleAddition() { Money sum= Money.dollar(5).plus(Money.dollar(5));
assertEquals(Money.dollar(10), sum); }
Make the test pass:1. Fake the implementation: Return "Money.dollar(10)“. 2. Obvious implementation.
MoneyMoney plus(Money addend) {
return new Money(amount + addend.amount, currency); }
All tests are passed!
Software Engineering, 2005 Test-Driven Development 111
Think Again About the Test: We know that we’ll need muti-currency arithmetic. Constraint: System must be unaware that it is dealing with
multiple currencies. Possible solution: Convert into a single currency.
Rejected. Too rigid.
We’ll try to rewrite the test so that it reflects the context of multi-currency arithmetic:
Question:
What is a multi-currency object?
Software Engineering, 2005 Test-Driven Development 112
Multi-currency objects:Example: ($2 + 3CHF) * 5
$5 + 10 CHF = $10 if rate is 2:1$5 + $5 = $10
A multi-currency object can be reduced to a single currency, if an exchange rate is given.
A multi-currency object can be added.
Require a multi-currency object Expression.think about an expression as a… Wallet!
Money is an atomic expression.Sum is an expression.Expressions can be reduced, WRT an exchange rate.
Software Engineering, 2005 Test-Driven Development 113
Rewrite the test to reflect multi-currencies context:
Replace:public void testSimpleAddition() {
Money sum= Money.dollar(5).plus(Money.dollar(5)); assertEquals(Money.dollar(10), sum);
}
With:public void testSimpleAddition() {
Money reduced = … addition of $5 expressions and reduction of the sum …;
assertEquals(Money.dollar(10), reduced); }
Software Engineering, 2005 Test-Driven Development 114
Who is responsible for exchange of Moneys?
The Bank, of course!Need a reduced Money: public void testSimpleAddition() {
…
Money reduced = bank.reduce(sum, “USD”);
assertEquals(Money.dollar(10), reduced); }
Need a benk:public void testSimpleAddition() {
... Bank bank= new Bank(); Money reduced = bank.reduce(sum, “USD”);
assertEquals(Money.dollar(10), reduced); }
Software Engineering, 2005 Test-Driven Development 115
A Revised Addition Test: Need a sum of Moneys which is an Expression:public void testSimpleAddition() {
...Expression sum= five.plus(five); Bank bank= new Bank(); Money reduced = bank.reduce(sum, “USD”); assertEquals(Money.dollar(10), reduced);
} Need 5 dollars Money:
public void testSimpleAddition() { Money five= Money.dollar(5); Expression sum= five.plus(five); Bank bank= new Bank(); Money reduced = bank.reduce(sum, “USD”); assertEquals(Money.dollar(10), reduced);
}
Software Engineering, 2005 Test-Driven Development 116
Make the Test compile:public void testSimpleAddition() {
Money five= Money.dollar(5); Expression sum= five.plus(five); Bank bank= new Bank(); Money reduced = bank.reduce(sum, “USD”); assertEquals(Money.dollar(10), reduced);
}Needed: interface Expression{} Money:
Expression plus(Money addend) { return new Money(amount + addend.amount, currency);
} class Money implements Expression class Bank{
Money reduce(Expression source, String to) { return null;
}}
Software Engineering, 2005 Test-Driven Development 117
Pass the Test – Fake it!public void testSimpleAddition() {
Money five= Money.dollar(5);
Expression sum= five.plus(five);
Bank bank= new Bank();
Money reduced = bank.reduce(sum, “USD”);
assertEquals(Money.dollar(10), reduced);
}
Bank
Money reduce(Expression source, String to) {
return Money.dollar(10);
}
All tests are passed!
Software Engineering, 2005 Test-Driven Development 118
Cycle Review: Reduced a big test to a smaller test that represented progress ($5 +
10 CHF to $5 + $5).
Thought carefully about the possible metaphors for our computation.
Rewrote our previous test based on our new metaphor.
Got the test to compile quickly.
Made it run.
Looked forward with a bit of trepidation to the refactoring necessary to make the implementation real.
Refactor!
Software Engineering, 2005 Test-Driven Development 119
15. Refactor: Remove Duplicationpublic void testSimpleAddition() {
Money five= Money.dollar(5); Expression sum= five.plus(five); Bank bank= new Bank(); Money reduced = bank.reduce(sum, “USD”); assertEquals(Money.dollar(10), reduced);
}
BankMoney reduce(Expression source, String to) {
return Money.dollar(10); }
and $10 is $5 + $5!
????? No idea how to refactor! ????? No idea how to replace the constant by a variable!
Software Engineering, 2005 Test-Driven Development 120
Work forward:public void testSimpleAddition() {
Money five= Money.dollar(5); Expression sum= five.plus(five); Bank bank= new Bank(); Money reduced = bank.reduce(sum, “USD”); assertEquals(Money.dollar(10), reduced);
}Think about the test implications and write more tests:1. The Money.plus() operation must return a non-atomic Expression: ---- a Sum. It cannot return a Money.2. Add a to-do item: Addition of identical currency is a Money.
$5 + 10 CHF = $10 if rate is 2:1$5 + $5 = $10Return Money from $5 + $5
Software Engineering, 2005 Test-Driven Development 121
Test – The sum of 2 Moneys is a Sum:
public void testPlusReturnsSum() {
Money five= Money.dollar(5);
Expression result= five.plus(five);
Sum sum= (Sum) result;
assertEquals(five, sum.augend);
assertEquals(five, sum.addend);
}
For compilation:
1. Class Sum with fields augend, addend.
2. Sum constructor.
3. Money.plus() returns a Sum not a Money (otherwise, ClassCastException – Money.plus() returns a Money not a Sum).
3. Sum implements Expression.
Software Engineering, 2005 Test-Driven Development 122
Compile Test – The sum of 2 Moneys is a Sum:
Sum
class Sum implements Expression{
Money augend;
Money addend;
Sum(Money augend, Money addend) { }
}
Money
Expression plus(Money addend) {
return new Sum(this, addend);
}
Compilation passes!
Software Engineering, 2005 Test-Driven Development 123
Pass Test – The sum of 2 Moneys is a Sum:
Just correct the Sum constructor:
Sum
Sum(Money augend, Money addend) {
this.augend= augend;
this.addend= addend;
}
All tests pass!
Software Engineering, 2005 Test-Driven Development 124
Back to Bank.reduce():
BankMoney reduce(Expression source, String to) {
return Money.dollar(10); }
Recall: we did not know how to refactor! But now we have a Sum concept Write a test for Bank.reduce():
public void testReduceSum() { Expression sum= new Sum(Money.dollar(3), Money.dollar(4));Bank bank= new Bank(); Money result= bank.reduce(sum, "USD");
assertEquals(Money.dollar(7), result); }
The test fails because…. 3 + 4 != 10
Software Engineering, 2005 Test-Driven Development 125
Pass the Test for Bank.reduce():
Bank
Money reduce(Expression source, String to) {
Sum sum= (Sum) source;
int amount= sum.augend.amount + sum.addend.amount;
return new Money(amount, to);
} All tests pass!
Problems: The cast. This code should work with any Expression.
The public fields, and two levels of references at that.
What about Bank reduction of a Money: Bank.reduce(Money)?
Software Engineering, 2005 Test-Driven Development 126
Refactor for Bank.reduce():
Bank
Money reduce(Expression source, String to) {
Sum sum= (Sum) source;
return sum.reduce(to);
}
Sum
public Money reduce(String to) {
int amount= augend.amount + addend.amount;
return new Money(amount, to);
} All tests pass!
Software Engineering, 2005 Test-Driven Development 127
A new item on the to-do list:
First write a …..test:
public void testReduceMoney() {
Bank bank= new Bank();
Money result= bank.reduce(Money.dollar(1), "USD"); assertEquals(Money.dollar(1), result);
}
$5 + 10 CHF = $10 if rate is 2:1$5 + $5 = $10Return Money from $5 + $5Bank.reduce(Money)
Software Engineering, 2005 Test-Driven Development 128
Pass Bank.reduce(Money) test:
Bank
Money reduce(Expression source, String to) {
if (source instanceof Money) return (Money) source; Sum sum= (Sum) source;
return sum.reduce(to); }
All tests pass! Really ugly! Use polymorphism rather than checking
classes explicitly.
Software Engineering, 2005 Test-Driven Development 129
Refactor for the Bank.reduce(Money) test:
Bank
Money reduce(Expression source, String to) {
if (source instanceof Money)
return (Money) source.reduce(to);
Sum sum= (Sum) source;
return sum.reduce(to); }
Money
public Money reduce(String to) {
return this;
} All tests pass!
Software Engineering, 2005 Test-Driven Development 130
Further Refactor for the Bank.reduce(Money) Test:
Add reduce(String) to the Expression interface,
Expression
Money reduce(String to);
Use polymorphism for reduce in Bank:
Bank
Money reduce(Expression source, String to) {
return source.reduce(to);
}
All tests pass!
Software Engineering, 2005 Test-Driven Development 131
Achieved in this cycle:
Didn't mark a test as done because the duplication had not been eliminated.
Worked forward instead of backward to realize the implementation.
Wrote a test to force the creation of an object we expected to need later (Sum).
Started implementing faster (the Sum constructor).
Implemented code with casts in one place, then moved the code where it belonged once the tests were running.
Introduced polymorphism to eliminate explicit class checking.
Software Engineering, 2005 Test-Driven Development 132
16. Currency exchange:
The exchange problem:
Reduce a Money with conversion – like:
“Reduce 2 francs to one dollar, if the exchange rate is 2:1”.
Start with ….. Writing a test!
$5 + 10 CHF = $10 if rate is 2:1$5 + $5 = $10Return Money from $5 + $5Bank.reduce(Money)Reduce Money with conversionMoney rounding?
Software Engineering, 2005 Test-Driven Development 133
Currency Exchange Test:Reduce a Money with conversion – like:“Reduce 2 francs to one dollar, if the exchange rate is 2:1”.
public void testReduceMoneyDifferentCurrency() { Bank bank= new Bank(); bank.addRate("CHF", "USD", 2); Money result= bank.reduce(Money.franc(2), "USD");
assertEquals(Money.dollar(1), result); }
Pass the test:Money
public Money reduce(String to) { int rate = (currency.equals("CHF") && to.equals("USD"))
? 2: 1;
return new Money(amount / rate, to); } All tests pass!
Software Engineering, 2005 Test-Driven Development 134
Refactor for the Currency exchange Test (1) – remove test-code duplication – the “2”:
Problem: Money knows about exchange rates!
Question: Who should know about exchange rates?
Answer: Only the Bank! Bank should be passed as an argument to Expression.reduce().
Needed changes: Caller: Bank.reduce(). Implementors: expression.reduce(), Sum.reduce(),
Money.reduce().
Software Engineering, 2005 Test-Driven Development 135
Refactor for the Currency exchange Test (2) – Pass Bank as a parameter:
BankMoney reduce(Expression source, String to) {
return source.reduce(this, to); }
ExpressionMoney reduce(Bank bank, String to);
Sumpublic Money reduce(Bank bank, String to) { int amount= augend.amount + addend.amount; return new Money(amount, to); }
Moneypublic Money reduce(Bank bank, String to) {
int rate = (currency.equals("CHF") && to.equals("USD")) ? 2
: 1; return new Money(amount / rate, to);
} All tests pass!
Software Engineering, 2005 Test-Driven Development 136
Refactor for the Currency exchange Test (3) – Calculate rate in bank:
Bankint rate(String from, String to) {
return (from.equals("CHF") && to.equals("USD")) ? 2 : 1;
}
Moneypublic Money reduce(Bank bank, String to) {
int rate = bank.rate(currency, to); return new Money(amount / rate, to);
} All tests pass!
Software Engineering, 2005 Test-Driven Development 137
Refactor for the Currency exchange Test (4) – Use a hash table for rates:
Still there is test-code duplication (the “2”). Keep a table of rates in the Bank and look up a rate when needed. Can use a hashtable that maps pairs of currencies to rates. What should be the key to the table? – a currency pair. How to implement the key?
• Two-element array containing the two currencies:
Test if Array.equals() checks to see if the elements are equal?
public void testArrayEquals() {
assertEquals(new Object[] {"abc"}, new Object[] {"abc"});
}
Test fails! • Create a real Pair object for the key.
Software Engineering, 2005 Test-Driven Development 138
Refactor for the Currency exchange Test (5) – Use a hash table for rates:
Create a real Pair object for the key. Because we are using Pairs as keys, we have to implement equals() and
hashCode().
Pairprivate class Pair {
private String from; private String to; public Pair(String from, String to) {
this.from= from; this.to= to; }
public boolean equals(Object object) { Pair pair= (Pair) object; return from.equals(pair.from) && to.equals(pair.to);
} public int hashCode() {
return 0; // A terrible hash value!}
}
Note: Implementation without a test! – forgive us!
Software Engineering, 2005 Test-Driven Development 139
Refactor for the Currency exchange Test (6) – Add the rates table to the Bank:
1. Store the rates:
Bank
private Hashtable rates= new Hashtable();
2. Set the rate when told:
Bank
void addRate(String from, String to, int rate) {
rates.put(new Pair(from, to), new Integer(rate));
}
3. Look up the rate when asked:
Bank
int rate(String from, String to) {
Integer rate= (Integer) rates.get(new Pair(from, to));
return rate.intValue();
} Tests fail!
Software Engineering, 2005 Test-Driven Development 140
Refactor for the Currency exchange Test (7) – Missing:handling self exchange rate:
1. Write a test:
public void testIdentityRate() {
assertEquals(1, new Bank().rate("USD", "USD"));
}
2. Pass it:
Bank
int rate(String from, String to) {
if (from.equals(to)) return 1;
Integer rate= (Integer) rates.get(new Pair(from, to));
return rate.intValue();
}
All tests pass!
Software Engineering, 2005 Test-Driven Development 141
Achievements in this cycle:
Added a parameter, in seconds, that we expected we would need.
Factored out the data duplication between code and tests.
Wrote a test (testArrayEquals) to check an assumption about the operation of Java.
Introduced a private helper class without distinct tests of its own.
Made a mistake in a refactoring and chose to forge ahead, writing another test to isolate the problem.
Software Engineering, 2005 Test-Driven Development 142
17. Mixed Currencies:
$5 + 10 CHF = $10 if rate is 2:1$5 + $5 = $10Return Money from $5 + $5Bank.reduce(Money)Reduce Money with conversionMoney rounding?
Software Engineering, 2005 Test-Driven Development 143
Mixed Currencies test:
public void testMixedAddition() {
Expression fiveBucks= Money.dollar(5);
Expression tenFrancs= Money.franc(10);
Bank bank= new Bank();
bank.addRate("CHF", "USD", 2);
Money result= bank.reduce(fiveBucks.plus(tenFrancs), "USD");
assertEquals(Money.dollar(10), result); }
• The test does not compile – because of Money - Expression disagreements.
• Approach: Simplify the test and then generalize.
Software Engineering, 2005 Test-Driven Development 144
A Simpler Mixed Currencies test:
public void testMixedAddition() {
Money fiveBucks= Money.dollar(5);
Money tenFrancs= Money.franc(10);
Bank bank= new Bank();
bank.addRate("CHF", "USD", 2);
Money result= bank.reduce(fiveBucks.plus(tenFrancs), "USD");
assertEquals(Money.dollar(10), result); }
• The test compiles but fails: result is $15.
• As if Sum.reduce() is not reducing its arguments.
Software Engineering, 2005 Test-Driven Development 145
Get Sum to reduce its components:
The older Sum.reduce():Sumpublic Money reduce(Bank bank, String to) {
int amount= augend.amount + addend.amount; return new Money(amount, to);
} The new Sum.reduce:
Sumpublic Money reduce(Bank bank, String to) {
int amount= augend.reduce(bank, to).amount + addend.reduce(bank, to).amount;
return new Money(amount, to); }
All tests pass!
Software Engineering, 2005 Test-Driven Development 146
Replacing Moneys by Expressions (1):
Method: from “leaves” upwards.
Sum
Expression augend;
Expression addend;
Arguments to Sum constructor:
Sum
Sum(Expression augend, Expression addend) {
this.augend= augend;
this.addend= addend;
} All tests pass!
Software Engineering, 2005 Test-Driven Development 147
Replacing Moneys by Expressions (2):
Money
Expression plus(Expression addend) {
return new Sum(this, addend);
}
Arguments to Sum constructor:
Money
Expression times(int multiplier) {
return new Money(amount * multiplier, currency);
} All tests pass!
Software Engineering, 2005 Test-Driven Development 148
Replacing Moneys by Expressions (3):
Get back the original mixed addition test:public void testMixedAddition() {
Expression fiveBucks= Money.dollar(5); Expression tenFrancs= Money.franc(10); Bank bank= new Bank(); bank.addRate("CHF", "USD", 2);Money result= bank.reduce(fiveBucks.plus(tenFrancs), "USD"); assertEquals(Money.dollar(10), result); }
Actually: Do it one by one – it’s TDD!1. Change the type of tenFrancs – All tests pass!2. Change the type of fiveBucks -- Test fails!
– plus() is not defined for expression.
Software Engineering, 2005 Test-Driven Development 149
Replacing Moneys by Expressions (4):
ExpressionExpression plus(Expression addend);
Moneypublic Expression plus(Expression addend) {
return new Sum(this, addend); }
Fake the implementation in Sum and add it to the next to-do:Sum
public Expression plus(Expression addend) { return null;
} All tests pass!
Software Engineering, 2005 Test-Driven Development 150
Achievements in this cycle:
Wrote the test we wanted, then backed off to make it achievable in one step.
Generalized (used a more abstract declaration) from the leaves back to the root (the test case).
Followed the compiler when we made a change (Expression fiveBucks), which caused changes to ripple (added plus() to Expression, and so on).
Software Engineering, 2005 Test-Driven Development 151
18. Abstraction, finally!
$5 + 10 CHF = $10 if rate is 2:1$5 + $5 = $10Return Money from $5 + $5Bank.reduce(Money)Reduce Money with conversionMoney rounding?Sum.plusExpression.times
Expression (and therefore Sum) still needs plus() and times():
Software Engineering, 2005 Test-Driven Development 152
Test for Sum.plus():
public void testSumPlusMoney() { Expression fiveBucks= Money.dollar(5); Expression tenFrancs= Money.franc(10); Bank bank= new Bank(); bank.addRate("CHF", "USD", 2); Expression sum= new Sum(fiveBucks, tenFrancs).plus(fiveBucks); Money result= bank.reduce(sum, "USD"); assertEquals(Money.dollar(15), result);
}
explicit Sum creation – for better communication!
Sum – same code as in Money:public Expression plus(Expression addend) {
return new Sum(this, addend); } All tests pass!
Software Engineering, 2005 Test-Driven Development 153
Achieving Expression.times():
In order to achieve Expression.times() we need Sum.times()
$5 + 10 CHF = $10 if rate is 2:1$5 + $5 = $10Return Money from $5 + $5Bank.reduce(Money)Reduce Money with conversionMoney rounding?Sum.plusExpression.times
Software Engineering, 2005 Test-Driven Development 154
Test for Sum.times():public void testSumTimes() {
Expression fiveBucks= Money.dollar(5); Expression tenFrancs= Money.franc(10); Bank bank= new Bank(); bank.addRate("CHF", "USD", 2); Expression sum= new Sum(fiveBucks, tenFrancs).times(2); Money result= bank.reduce(sum, "USD"); assertEquals(Money.dollar(20), result);
}
SumExpression times(int multiplier) {
return new Sum(augend.times(multiplier),addend.times(multiplier)); }
Times() must be defined in Expression -- augend and addend are Expressions:Expression
Expression times(int multiplier); All tests pass!
Software Engineering, 2005 Test-Driven Development 155
Achieving Money + Money is a Money:
$5 + 10 CHF = $10 if rate is 2:1$5 + $5 = $10Return Money from $5 + $5Bank.reduce(Money)Reduce Money with conversionMoney rounding?Sum.plusExpression.times
Software Engineering, 2005 Test-Driven Development 156
Test for Money + Money = Money:
public void testPlusSameCurrencyReturnsMoney() {
Expression sum= Money.dollar(1).plus(Money.dollar(1)); assertTrue(sum instanceof Money);
}
An ugly test – tests the implementation and not the observable!
Code to modify:
Money
public Expression plus(Expression addend) {
return new Sum(this, addend);
}
Test fails! We hate it! we cross it! All tests pass!
Software Engineering, 2005 Test-Driven Development 157
The end:
$5 + 10 CHF = $10 if rate is 2:1$5 + $5 = $10Return Money from $5 + $5Bank.reduce(Money)Reduce Money with conversionMoney rounding?Sum.plusExpression.times
Software Engineering, 2005 Test-Driven Development 158
Summary – 3 basics of TDD:
The three approaches to making a test work cleanly—fake it, triangulation, and obvious implementation.
Removing duplication between test and code as a way to drive the design.
The ability to control the gap between tests to increase traction when the road gets slippery and cruise faster when conditions are clear.
Top Related