Software Engineering, 2005Test-Driven Development 1 Test-Driven Development Software Engineering Test-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, 2005Test-Driven Development 2 What is Test-Driven Development? l An iterative technique for developing software. l 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, 2005Test-Driven Development 3 What is TDD? l Must be learned and practiced. If it feels natural at first, you’re probably doing it wrong. l More productive than debug-later programming. l It’s an addiction rather than a discipline – Kent Beck Grenning
Software Engineering, 2005Test-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, 2005Test-Driven Development 5 TDD Development Cycle Start Refactor as needed Write a test for a new capability Run all tests to see them pass Write the code Run the test and see it fail Fix compile errors Compile Grenning Run all tests to see them pass
Software Engineering, 2005Test-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, 2005Test-Driven Development 7 TDD Process Beck l Once a test passes, it is re-run with every change. l Broken tests are not tolerated. l Side-effects defects are detected immediately. l Assumptions are continually checked. l Automated tests provide a safety net that gives you the courage to refactor.
Software Engineering, 2005Test-Driven Development 8 TDD Process l What do you test? … everything that could possibly break - Ron Jefferies l Don’t test anything that could not possibly break (always a judgment call) Example: Simple accessors and mutators Beck
Software Engineering, 2005Test-Driven Development 9 TDD Process 1.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, 2005Test-Driven Development 10 Test Selection l Which test should you pick from the list? Pick a test that will teach you something and that you are confident you can implement. l Each test should represent one step toward your overall goal. l How should the running of tests affect one another? Not at all – isolated tests. Beck
Software Engineering, 2005Test-Driven Development 11 Assert First l Where should you start building a system? With the stories you want to be able to tell about the finished system. l Where should you start writing a bit of functionality? With the tests you want to pass on the finished code. l Where should you start writing a test? With the asserts that will pass when it is done. Beck
Software Engineering, 2005Test-Driven Development 12 TDD techniques: Beck l Fake It (‘Til You Make It) l Triangulate l Obvious Implementation
Software Engineering, 2005Test-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, 2005Test-Driven Development 14 “Expected surprises”: l How each test can cover a small increment of functionality. l How small and ugly the changes can be to make the new tests run. l How often the tests are run. l How many teensy-weensy steps make up the refactorings.
Software Engineering, 2005Test-Driven Development 15 Motivating story l WyCash – A company that sold bond portfolio management systems in US dollars. l Requirement: Add multi-currencies. l Method used: Replace the former basic Dollar objects by Create Multi-Currency Money objects. Instead of – revise all existing services.
Software Engineering, 2005Test-Driven Development 16 Requirement – Create a report: Like this: l InstrumentSharesPrice Total IBM USD USD Novartis CHF CHF Total USD Exchange rates: l FromToRate CHF USD1.5 l $ CHF = $10 if rate is 2:1 $5 * 2 = $10
Software Engineering, 2005Test-Driven Development 17 The 1st to-do list: l We need to be able to add amounts in two different currencies and convert the result given a set of exchange rates. l We need to be able to multiply an amount (price per share) by a number (number of shares) and receive an amount. $ CHF = $10 if rate is 2:1 $5 * 2 = $10
Software Engineering, 2005Test-Driven Development 18 The First test: l We work on multiplication first, because it looks simpler. l What object we need – wrong question! l What test we need? public void testMultiplication() { Dollar five= new Dollar(5); five.times(2); assertEquals(10, five.amount); }
Software Engineering, 2005Test-Driven Development 19 2 nd to-do list: $ CHF = $10 if rate is 2:1 $5 * 2 = $10 Make "amount" private Dollar 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, 2005Test-Driven Development 20 Make the test compile: class Dollar{ public int amount; public Dollar(int amount){} public void times(int multiplier){} }
Software Engineering, 2005Test-Driven Development 21 Test fails. l Is it bad? l 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, 2005Test-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, 2005Test-Driven Development 23 The refactor (generalize) step: l Quickly add a test. l Run all tests and see the new one fail. l Make a little change. l Run all tests and see them all succeed. l 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, 2005Test-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, 2005Test-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, 2005Test-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, 2005Test-Driven Development 27 2 nd to-do list: $ CHF = $10 if rate is 2:1 $5 * 2 = $10 Make "amount" private Dollar side-effects? Money rounding? The current to-do list:
Software Engineering, 2005Test-Driven Development 28 Our achievements: l Made a list of the tests we knew we needed to have working. l Told a story with a snippet of code about how we wanted to view one operation. l Made the test compile with stubs. l Made the test run by committing horrible sins. l Gradually generalized the working code, replacing constants with variables. l Added items to our to-do list rather than addressing them all at once.
Software Engineering, 2005Test-Driven Development 29 Our method: l 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. l 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. l 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, 2005Test-Driven Development 30 2 nd to-do list: $ CHF = $10 if rate is 2:1 $5 * 2 = $10 Make "amount" private Dollar 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, 2005Test-Driven Development 31 Degenerate objects: l 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, 2005Test-Driven Development 32 Degenerate objects: l 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, 2005Test-Driven Development 33 Degenerate objects: l 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, 2005Test-Driven Development 34 Achieved item in the to-do list: $ CHF = $10 if rate is 2:1 $5 * 2 = $10 Make "amount" private Dollar 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, 2005Test-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. l Translated a design objection (side effects) into a test case that failed because of the objection. l Got the code to compile quickly with a stub implementation. l Made the test work by typing in what seemed to be the right code.
Software Engineering, 2005Test-Driven Development 36 Two strategies for test passing: l Fake It— Return a constant and gradually replace constants with variables until you have the real code. l Use Obvious Implementation— Type in the real implementation.
Software Engineering, 2005Test-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, 2005Test-Driven Development 38 3 rd to-do list: l If Dollar is the key to a hash table, then implementing equals() requires also an implementation of hashcode() (the equals – hash tables contract). $ CHF = $10 if rate is 2:1 $5 * 2 = $10 Make "amount" private Dollar side-effects? Money rounding? equals() hashcode()
Software Engineering, 2005Test-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, 2005Test-Driven Development 40 Triangulation generalization technique: l Triangulation is a generalization that is motivated by 2 examples or more. l 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, 2005Test-Driven Development 41 Triangulation considrations: l Triangulation could have been used in the generalization of Dollar.times(). Instead of following test-model duplications we could have invented another multiplication example. l Beck’s suggestion: Use triangulation only if you do not know how to refactor using duplication removal. l Triangulation means trying some variation in a dimension of the design.
Software Engineering, 2005Test-Driven Development 42 Review of handling the 4 th to-do list: l Noticed that our design pattern (Value Object) implied an operation. l Tested for that operation. l Implemented it simply. l Didn't refactor immediately, but instead tested further. l Refactored to capture the two cases at once.
Software Engineering, 2005Test-Driven Development 43 4 th to-do list: Equality requires also: l Comparison with null. l Comparison with other, non Dollar objects. $ CHF = $10 if rate is 2:1 $5 * 2 = $10 Make "amount" private Dollar side-effects? Money rounding? equals() hashcode() Equal null Equal object
Software Engineering, 2005Test-Driven Development 44 Privacy: 5 th to-do list: Equality of Dollar objects enables making the instance variable amount private: $ CHF = $10 if rate is 2:1 $5 * 2 = $10 Make "amount" private Dollar side-effects? Money rounding? equals() hashcode() Equal null Equal object
Software Engineering, 2005Test-Driven Development 45 Improve the multiplication test: l 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); } with public 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, 2005Test-Driven Development 46 Improve the multiplication test: l 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, 2005Test-Driven Development 47 Achieved the 5 th to-do list: $ CHF = $10 if rate is 2:1 $5 * 2 = $10 Make "amount" private Dollar side-effects? Money rounding? equals() hashcode() Equal null Equal object
Software Engineering, 2005Test-Driven Development 48 Achieved the 5 th 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 5 th to-do list step: l Used functionality just developed to improve a test. l Noticed that if two tests fail at once we're sunk. l Proceeded in spite of the risk. l 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, 2005Test-Driven Development 49 Frank-ly speaking: the 6 th 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. $ CHF = $10 if rate is 2:1 $5 * 2 = $10 Make "amount" private Dollar side-effects? Money rounding? equals() hashcode() Equal null Equal object 5 CHF * 2 = 10 CHF
Software Engineering, 2005Test-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, 2005Test-Driven Development 51 Pass the Franc multiplication test: Recall the cycle steps: l Write a test. l Make it compile. l Run it to see that it fails. l Make it run. l Remove duplication. We now have to accomplish step 4. We’ll copy the Dollar class! And create robust duplication!
Software Engineering, 2005Test-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, 2005Test-Driven Development 53 Refactor – Following Franc Multiplication test: We created robust duplication: l Classes Dollar and Franc. l Common equals. l 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, 2005Test-Driven Development 54 Equality for all: the 7 th to-do list: $ CHF = $10 if rate is 2:1 $5 * 2 = $10 Make "amount" private Dollar side-effects? Money rounding? equals() hashcode() Equal null Equal object 5 CHF * 2 = 10 CHF Dollar/Franc duplication Common equals Common times
Software Engineering, 2005Test-Driven Development 55 Cleanup duplication: l Insert a common superclass Money for the Dollar and Franc classes. l We do it gradually, applying the tests all the time: 1.class MoneyAll 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, 2005Test-Driven Development 56 Work on equals: 4.Dollar public boolean equals(Object object) { Money dollar = (Dollar) object; return amount == dollar.amount; } All tests pass. 5. Dollar public boolean equals(Object object) { Money dollar = (Money) object; return amount == dollar.amount; } All tests pass. 6. Dollar public boolean equals(Object object) { Money money = (Money) object; return amount == money. amount; } All tests pass.
Software Engineering, 2005Test-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, 2005Test-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, 2005Test-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, 2005Test-Driven Development Franc public boolean equals(Object object) { Money franc = (Franc object; return amount == franc.amount; } All tests pass. 11. Franc public boolean equals(Object object) { Money franc = (Money) object; return amount == dollar.amount; } All tests pass. 12. Franc public boolean equals(Object object) { Money money = (Money) object; return amount == money. amount; } All tests pass. Eliminate Franc.equals():
Software Engineering, 2005Test-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: l Stepwise moved common code from one class (Dollar) to a superclass (Money). l Made a second class (Franc) a subclass also. l Reconciled two implementations—equals()—before eliminating the redundant one. Eliminate Franc.equals():
Software Engineering, 2005Test-Driven Development 62 Equality for all: the 8 th to-do list: $ CHF = $10 if rate is 2:1 $5 * 2 = $10 Make "amount" private Dollar side-effects? Money rounding? equals() hashcode() Equal null Equal object 5 CHF * 2 = 10 CHF Dollar/Franc duplication Common equals Common times Compare Francs with Dollars
Software Engineering, 2005Test-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, 2005Test-Driven Development 64 Comparing Dollars with Francs: l The test fails because equality does not check for being the same currency – Dollars ARE Francs!. l But there are no currency objects a new concept is needed. l 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, 2005Test-Driven Development 65 Comparing Dollars with Francs cycle: l Took an objection that was bothering us and turned it into a test. l Made the test run a reasonable, but not perfect way — getClass(). l Decided not to introduce more design until we had a better motivation.
Software Engineering, 2005Test-Driven Development 66 Making Objects: the 9 th to-do list: $ CHF = $10 if rate is 2:1 $5 * 2 = $10 Make "amount" private Dollar side-effects? Money rounding? equals() hashcode() Equal null Equal object 5 CHF * 2 = 10 CHF Dollar/Franc duplication Common equals Common times Compare Francs with Dollars Currency?
Software Engineering, 2005Test-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, 2005Test-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, 2005Test-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, 2005Test-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, 2005Test-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, 2005Test-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, 2005Test-Driven Development 73 Remove explicit references to Dollar Achieved: l No client code knows that there is a subclass called Dollar. l 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, 2005Test-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, 2005Test-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, 2005Test-Driven Development 76 Achievements in this cycle: l Took a step toward eliminating duplication by reconciling the signatures of two variants of the same method—times(). l Moved at least a declaration of the method to the common superclass. l Decoupled test code from the existence of concrete subclasses by introducing factory methods. l Noticed that when the subclasses disappear some tests will be redundant, but took no action.
Software Engineering, 2005Test-Driven Development 77 Currency: the 10 th to-do list: $ CHF = $10 if rate is 2:1 $5 * 2 = $10Delete testFrancMultiplication? Make "amount" private Dollar side-effects? Money rounding? equals() hashcode() Equal null Equal object 5 CHF * 2 = 10 CHF Dollar/Franc duplication Common equals Common times Compare Francs with Dollars Currency?
Software Engineering, 2005Test-Driven Development 78 Currency? l How to implement currency? – No, no. Wrong question! l How to test currencies! l In the future – complicated objects with flyweight factories (to ensure controlled creation). l Now – only strings: public void testCurrency() { assertEquals("USD", Money.dollar(1).currency()); assertEquals("CHF", Money.franc(1).currency()); }
Software Engineering, 2005Test-Driven Development 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, 2005Test-Driven Development Refactoring: Achieving a common implementation for currency() in Dollar and Franc: Franc private String currency; Franc(int amount) { this.amount = amount; currency = "CHF"; } String currency() { return currency; } Dollar private String currency; Dollar(int amount) { this.amount = amount; currency = “USD"; } String currency() { return currency; }
Software Engineering, 2005Test-Driven Development Refactoring: Push up the variable and the implementation of currency(): Money protected String currency; String currency() { return currency; } Achieved: All tests pass!
Software Engineering, 2005Test-Driven Development 82 $ CHF = $10 if rate is 2:1 $5 * 2 = $10Delete testFrancMultiplication? Make "amount" private Dollar side-effects? Money rounding? equals() hashcode() Equal null Equal object 5 CHF * 2 = 10 CHF Dollar/Franc duplication Common equals Common times Compare Francs with Dollars Currency? Currency achieved!
Software Engineering, 2005Test-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); } Franc Money times(int multiplier) { return new Franc(amount * multiplier, null); }
Software Engineering, 2005Test-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, 2005Test-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, 2005Test-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, 2005Test-Driven Development 87 Refactoring: Push up the implementation of the constructors: The 2 constructors are identical! Push up the implementation: Money Money(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, 2005Test-Driven Development 88 Achievements in this cycle: l Were a little stuck on big design ideas, so we worked on something small we noticed earlier. l Reconciled the two constructors by moving the variation to the caller (the factory method). l Interrupted a refactoring for a little twist, using the factory method in times(). l Repeated an analogous refactoring (doing to Dollar what we just did to Franc) in one big step. l Pushed up the identical constructors.
Software Engineering, 2005Test-Driven Development 89 Interesting Times: the 11 th to-do list: $ CHF = $10 if rate is 2:1 $5 * 2 = $10Delete testFrancMultiplication? Make "amount" private Dollar side-effects? Money rounding? equals() hashcode() Equal null Equal object 5 CHF * 2 = 10 CHF Dollar/Franc duplication Common equals Common times Compare Francs with Dollars Currency?
Software Engineering, 2005Test-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, 2005Test-Driven Development 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, 2005Test-Driven Development 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, 2005Test-Driven Development Refactoring: Making Dollar.times() and Franc.times() identical: l Try to replace Franc with Money. l Does it matter? ask the tests! Franc Money times(int multiplier) { return new Money(amount * multiplier, currency); } Money cannot be abstract: Money class Money Money times(int multiplier) { return null; }
Software Engineering, 2005Test-Driven Development Refactoring: Making Dollar.times() and Franc.times() identical: Red bar! Error message: "expected: but was: ". Not as helpful. Define toString() for a better error message: public String toString() { return amount + " " + currency; } Code without a test? Exception cause: l Results on the screen. l toString() is used only for debug output. the risk of it failing is low. l Already have a red bar! Prefer not to write a test within a red bar.
Software Engineering, 2005Test-Driven Development Refactoring: Making Dollar.times() and Franc.times() identical: New error message: "expected: but was: ". l Right data but wrong class: Money instead of Franc. l Problem results from testFrancMultiplcation(), that is based on the implementation of equals(): public void testFrancMultiplication() { Money five= Money.franc(5); a Money assertEquals(Money.franc(10), five.times(2)); assertEquals(Money.franc(15), five.times(3)); } Money public boolean equals(Object object) { Money money = (Money) object; return amount == money.amount && getClass().equals(money.getClass()); }
Software Engineering, 2005Test-Driven Development Refactoring: Making Dollar.times() and Franc.times() identical: l Should check for same currencies not same classes. l A new requirement – Assert Money(10, “CHF”), Franc(10, “CHF) as equal! l TDD Methodology write a test. l 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, 2005Test-Driven Development 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, 2005Test-Driven Development 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, 2005Test-Driven Development 99 Achievements in this cycle: l Reconciled two methods—times()—by first inlining the methods they called and then replacing constants with variables. l Wrote a toString() without a test just to help us debug. l Tried a change (returning Money instead of Franc) and let the tests tell us whether it worked. l Backed out an experiment and wrote another test. Making the test work made the experiment work.
Software Engineering, 2005Test-Driven Development 100 Eliminate subclasses: the 12 th to-do list: $ CHF = $10 if rate is 2:1 $5 * 2 = $10Delete testFrancMultiplication? Make "amount" private Dollar side-effects? Money rounding? equals() hashcode() Equal null Equal object 5 CHF * 2 = 10 CHF Dollar/Franc duplication Common equals Common times Compare Francs with Dollars Currency?
Software Engineering, 2005Test-Driven Development 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"); } l Change the reference to Dollar in the factory method: static Money dollar(int amount) { return new Money(amount, "USD"); }
Software Engineering, 2005Test-Driven Development 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, 2005Test-Driven Development 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, 2005Test-Driven Development 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, 2005Test-Driven Development 105 Achievements in this cycle: l Finished gutting subclasses and deleted them. l Eliminated tests that made sense with the old code structure but were redundant with the new code structure.
Software Engineering, 2005Test-Driven Development 106 The 13 th to-do list: $ CHF = $10 if rate is 2:1 $5 * 2 = $10Delete testFrancMultiplication? Make "amount" private Dollar side-effects? Money rounding? equals() hashcode() Equal null Equal object 5 CHF * 2 = 10 CHF Dollar/Franc duplication Common equals Common times Compare Francs with Dollars Currency?
Software Engineering, 2005Test-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, 2005Test-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, 2005Test-Driven Development Addition – at last: A new to-do list: $ CHF = $10 if rate is 2:1 $5 + $5 = $10 Remove achieved items. Handle simple items. The new list: $ CHF = $10 if rate is 2:1 Start with a simpler addition:
Software Engineering, 2005Test-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. Money Money plus(Money addend) { return new Money(amount + addend.amount, currency); } All tests are passed!
Software Engineering, 2005Test-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, 2005Test-Driven Development 112 Multi-currency objects: Example:($2 + 3CHF) * 5 $ 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, 2005Test-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, 2005Test-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, 2005Test-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, 2005Test-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, 2005Test-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, 2005Test-Driven Development 118 Cycle Review: l Reduced a big test to a smaller test that represented progress ($ CHF to $5 + $5). l Thought carefully about the possible metaphors for our computation. l Rewrote our previous test based on our new metaphor. l Got the test to compile quickly. l Made it run. l Looked forward with a bit of trepidation to the refactoring necessary to make the implementation real. Refactor!
Software Engineering, 2005Test-Driven Development Refactor: Remove Duplication 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); } and $10 is $5 + $5! ????? No idea how to refactor! ????? No idea how to replace the constant by a variable!
Software Engineering, 2005Test-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. $ CHF = $10 if rate is 2:1 $5 + $5 = $10 Return Money from $5 + $5
Software Engineering, 2005Test-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, 2005Test-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, 2005Test-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, 2005Test-Driven Development 124 Back to Bank.reduce(): Bank Money reduce(Expression source, String to) { return Money.dollar(10); } l Recall: we did not know how to refactor! l 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); } l The test fails because… != 10
Software Engineering, 2005Test-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: l The cast. This code should work with any Expression. l The public fields, and two levels of references at that. l What about Bank reduction of a Money: Bank.reduce(Money)?
Software Engineering, 2005Test-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, 2005Test-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); } $ CHF = $10 if rate is 2:1 $5 + $5 = $10 Return Money from $5 + $5 Bank.reduce(Money)
Software Engineering, 2005Test-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! l Really ugly! l Use polymorphism rather than checking classes explicitly.
Software Engineering, 2005Test-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, 2005Test-Driven Development 130 Further Refactor for the Bank.reduce(Money) Test: l Add reduce(String) to the Expression interface, Expression Money reduce(String to); l Use polymorphism for reduce in Bank: Bank Money reduce(Expression source, String to) { return source.reduce(to); } All tests pass!
Software Engineering, 2005Test-Driven Development 131 Achieved in this cycle: l Didn't mark a test as done because the duplication had not been eliminated. l Worked forward instead of backward to realize the implementation. l Wrote a test to force the creation of an object we expected to need later (Sum). l Started implementing faster (the Sum constructor). l Implemented code with casts in one place, then moved the code where it belonged once the tests were running. l Introduced polymorphism to eliminate explicit class checking.
Software Engineering, 2005Test-Driven Development 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! $ CHF = $10 if rate is 2:1 $5 + $5 = $10 Return Money from $5 + $5 Bank.reduce(Money) Reduce Money with conversion Money rounding?
Software Engineering, 2005Test-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, 2005Test-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: l Caller: Bank.reduce(). l Implementors: expression.reduce(), Sum.reduce(), Money.reduce().
Software Engineering, 2005Test-Driven Development 135 Refactor for the Currency exchange Test (2) – Pass Bank as a parameter: Bank Money reduce(Expression source, String to) { return source.reduce(this, to); } Expression Money reduce(Bank bank, String to); Sum public Money reduce(Bank bank, String to) { int amount= augend.amount + addend.amount; return new Money(amount, to); } Money public 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, 2005Test-Driven Development 136 Refactor for the Currency exchange Test (3) – Calculate rate in bank: Bank int rate(String from, String to) { return (from.equals("CHF") && to.equals("USD")) ? 2 : 1; } Money public Money reduce(Bank bank, String to) { int rate = bank.rate(currency, to); return new Money(amount / rate, to); } All tests pass!
Software Engineering, 2005Test-Driven Development 137 Refactor for the Currency exchange Test (4) – Use a hash table for rates: l Still there is test-code duplication (the “2”). l Keep a table of rates in the Bank and look up a rate when needed. l Can use a hashtable that maps pairs of currencies to rates. l What should be the key to the table? – a currency pair. l 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, 2005Test-Driven Development 138 Refactor for the Currency exchange Test (5) – Use a hash table for rates: l Create a real Pair object for the key. l Because we are using Pairs as keys, we have to implement equals() and hashCode (). Pair private 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, 2005Test-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, 2005Test-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, 2005Test-Driven Development 141 Achievements in this cycle: l Added a parameter, in seconds, that we expected we would need. l Factored out the data duplication between code and tests. l Wrote a test (testArrayEquals) to check an assumption about the operation of Java. l Introduced a private helper class without distinct tests of its own. l Made a mistake in a refactoring and chose to forge ahead, writing another test to isolate the problem.
Software Engineering, 2005Test-Driven Development Mixed Currencies: $ CHF = $10 if rate is 2:1 $5 + $5 = $10 Return Money from $5 + $5 Bank.reduce(Money) Reduce Money with conversion Money rounding?
Software Engineering, 2005Test-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, 2005Test-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, 2005Test-Driven Development 145 Get Sum to reduce its components: The older Sum.reduce(): Sum public Money reduce(Bank bank, String to) { int amount= augend.amount + addend.amount; return new Money(amount, to); } The new Sum.reduce: Sum public 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, 2005Test-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, 2005Test-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, 2005Test-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, 2005Test-Driven Development 149 Replacing Moneys by Expressions (4): Expression Expression plus(Expression addend); Money public 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, 2005Test-Driven Development 150 Achievements in this cycle: l Wrote the test we wanted, then backed off to make it achievable in one step. l Generalized (used a more abstract declaration) from the leaves back to the root (the test case). l Followed the compiler when we made a change (Expression fiveBucks), which caused changes to ripple (added plus() to Expression, and so on).
Software Engineering, 2005Test-Driven Development Abstraction, finally! $ CHF = $10 if rate is 2:1 $5 + $5 = $10 Return Money from $5 + $5 Bank.reduce(Money) Reduce Money with conversion Money rounding? Sum.plus Expression.times Expression (and therefore Sum) still needs plus() and times():
Software Engineering, 2005Test-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, 2005Test-Driven Development 153 Achieving Expression.times(): In order to achieve Expression.times() we need Sum.times() $ CHF = $10 if rate is 2:1 $5 + $5 = $10 Return Money from $5 + $5 Bank.reduce(Money) Reduce Money with conversion Money rounding? Sum.plus Expression.times
Software Engineering, 2005Test-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); } Sum Expression 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, 2005Test-Driven Development 155 Achieving Money + Money is a Money: $ CHF = $10 if rate is 2:1 $5 + $5 = $10 Return Money from $5 + $5 Bank.reduce(Money) Reduce Money with conversion Money rounding? Sum.plus Expression.times
Software Engineering, 2005Test-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, 2005Test-Driven Development 157 The end: $ CHF = $10 if rate is 2:1 $5 + $5 = $10 Return Money from $5 + $5 Bank.reduce(Money) Reduce Money with conversion Money rounding? Sum.plus Expression.times
Software Engineering, 2005Test-Driven Development 158 Summary – 3 basics of TDD: l The three approaches to making a test work cleanly— fake it, triangulation, and obvious implementation. l Removing duplication between test and code as a way to drive the design. l The ability to control the gap between tests to increase traction when the road gets slippery and cruise faster when conditions are clear.