The goal is to cover all needed steps to apply TDD how suggest by Kent Beck in Test Driven Development: By Example and provide guide lines for the developers.
-
Write Unit Test
- Run test
- Test doesn't compile
-
Use IDE to create
class
,constructor
,methods
andfields
- Run test
- Bingo! Test fails
N.B. Now we have a concrete measure of failure.
-
The smallest change in order that our test to pass
public int amount = 10;
-
Remove duplication between the data in the test and the data in the code.
- Remove duplication, moving the setting of the
amount
from object initialization to thetimes()
method.
public void times(int multiplier) { amount = 5 * 2; }
4.2 substitute the parameters
amount
andmultiplier
for the constants - Remove duplication, moving the setting of the
-
Find eventual implementation error
@Test public void testMultiplication(){ Dollar five = new Dollar(5); five.times(2); assertEquals(10, five.amount); five.times(3); assertEquals(15, five.amount); }
N.B. Test fails! After the first call to
times()
five isn't five aby more, it's really ten. -
We return a new object from
times()
, then we can multiply our originalamount
and never have it change.
Use Value Objects
in order to use objects as value. The values of the instance variables of the object never change once they have been set in the constructor.
- You needn't worry about aliasing.
- All operations must return a new object.
- Should implement
equals()
. - Use Triangulation and generalize
equals()
and other methods that need it.- We generalize code when we have two examples or more.
- Defined equality, we can use it to make our tests more "speaking".
Dollar.times()
should returnDollar
- Rewrite assertions to compare
Dollar
s toDollar
s
Dollar
is now the only class using its amount instance variable, so we can make it private
- We need to have an object like
Dollar
, but to represent francs. - What short step will get us to a green bar? Copying the
Dollar
code and replacingDollar
withFranc
We are going to find a common superclass for the two class.
- Create
Money
class as superclass. - Move the
amount
instance variable up to superclass and the visibility has to change fromprivate
to `protected. - Move up
equals()
and change the declaration of the temporary variable
- What happens when we compare
Francs
andDollars
? It fails! - The equality code needs to compare the class of the two objects.
- Reconcile the two implementations of
time()
, making them both returnMoney
. - Introduce a Factory method in superclass that returns subclasses.
- Make
Money
abstract and declareMoney.times()
.
No client code knows that there is a subclass called
Dollar
orFranc
!
We may want to have complicated objects representing currencies, with flyweight factories to ensure we create no more objects than we really need.
- Declare
currency()
inMoney
.abstract String currency();
- Implement it in both subclasses.
- We want the same implementation for both classes.
- Store
currency
in an instance variable.
private String currency; public Dollar(int amount) { this.amount = amount; currency = "USD"; } public String currency() { return currency; }
private String currency; public Franc(int amount) { this.amount = amount; currency = "CHD"; } public String currency() { return currency; }
- Push up declaration of the variable and the implementation of
currency()
.
protected String currency; String currency() { return currency; }
- Store
- Add parameter
currency
to the constructor. - Move constant strings
USD
andCHF
to the static factory methods.times()
is calling the constructor. Clean uptimes()
calling the factory method
- The two constructors are now identical, so we can push up the implementation.
- The two implementations of
times()
are close, but not identical.- We have to go backward to go forward:
- inline factory methods
- use
currency
instance variable.
public Money times(int multiplier) { return new Dollar(this.amount * multiplier, this.currency); }
public Money times(int multiplier) { return new Franc(this.amount * multiplier, this.currency); }
- We have to go backward to go forward:
- Change
Franc.times()
andDollar.times()
to returnMoney
.public Money times(int multiplier) { return new Money(amount * multiplier, this.currency); }
- Change
Money
to concrete class and improveequals()
. - Now the two implementations are identical. Push up
times()
implementation.
Dollar
andFranc
have only their constructors.- A constructor is not reason enough to have a subclass, we want to delete the subclasses!
- We can replace references to the subclasses with references to the superclass without changing the meaning of the code.
- Delete duplicated tests.
- Add a new feature about addition.
- Start with a simple example
@Test public void testSimpleAddition() { Money sum = Money.dollar(5).plus(Money.dollar(5)); assertEquals(Money.dollar(10), sum); }
- The implementation will be:
public Money plus(Money added) { return new Money(amount + added.amount, currency); }
- How are we going to represent multi-currency arithmetic?
- The solution is to create un object that acts like a
Money
but represents the sum of twoMoney
s. - A metaphor is expression
- where
Money
is the atomic form of an expression. - Operations result in
Expression
s, one of which will be a Sum.
- where
- Apply this metaphor to our test
@Test public void testSimpleAddition() { ... assertEquals(Money.dollar(10), reduced); }
- The
reduced Expression
is created by applying exchange rates to anExpression
. What in the real worl applies exchange rates? A bank
@Test public void testSimpleAddition() { ... Bank bank = new Bank(); Money reduced = bank.reduce(sum, "USD"); assertEquals(Money.dollar(10), reduced); }
- The sum of two
Money
s should be anExpression
@Test public void testSimpleAddition() { ... Expressions sum = five.plus(five); Bank bank = new Bank(); Money reduced = bank.reduce(sum, "USD"); assertEquals(Money.dollar(10), reduced); }
- We need an interface
Expression
. Money.plus()
needs to return anExpression
.- We need a
Bank
class which fakes the implementation ofreduce()
method.
- The solution is to create un object that acts like a
Money.plus()
needs to return a realExpression
-Sum
, not just aMoney
.@Test 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); }
- Create
Sum
class with two fields (augend
andaddend
), a constructor and needs to be a kind ofExpression
. - Now
Bank.reduce()
is being passed aSum
@Test 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); }
- Implement
Bank.reduce()
public Money reduce(Expression source, String to){ Sum sum = (Sum) source; int amount = sum.augend.amount + sum.addend.amount; return new Money(amount, to); }
- The cast. This code should work with any
Expression
. - The public fields, and two levels of references at that.
- The cast. This code should work with any
- Move the body of the method
Bank.reduce()
toSum
and get rid of some of visible fields. - How are we going to test/implement
Bank.reduce()
when the argument is a money?@Test public void testReduceMoney() { Bank bank = new Bank(); Money result = bank.reduce(Money.dollar(1), "USD"); assertEquals(Money.dollar(1), result); }
- Implement
Bank.reduce()
public Money reduce(Expression source, String to){ if(source instanceof Money) return (Money) source; Sum sum = (Sum) source; return sum.reduce(to); }
Any time we are checking classes explicitly, we should be using polymorphism instead!
Money
implementsreduce()
and addreduce(String)
toExpression
interface. Then Eliminate all casts and class checks.
- Implement