First of all, sorry for the long absence. Our training schedule has been wall-to-wall, and when one of us had a brief gap the other has always been busy.
It has given us time to think, however. Long airplane rides and such. :)
We're been playing around with an idea we're calling (for the moment) TDD Mark 3 (the notion that TDD is not about testing but rather about specification being TDD Mark 2). To give you an idea of what we're thinking, let's look at an example of TDD Mark 2 as we've been writing tests up to this point, and then refactor it to the TDD Mark 3 style.
[TestClass]
public class FundLoaderTDD
{
[TestMethod]
public void TestMinimalFunds()
{
Assert.AreEqual(2000, Card.MINIMAL_FUNDS);
}
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
uint minimalFunds = Card.MINIMAL_FUNDS;
Card card = new Card(/*card holder's details*/);
card.LoadFunds(minimalFunds);
uint insufficientFunds = minimalFunds - 1;
card = new Card(/**/);
try
{
card.LoadFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Card.InsufficientFundsException).Name());
}
catch (Card.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
}
The meaning here is very clear: the LoadFunds() method of Card will throw an InsufficientFundsException if you try to load an amount less than the minimal allowed value. We also show that if the minimal amount is loaded, an exception is not thrown. This constitutes a very typical specification of a behavioral boundary anchored at the value MINIMAL_FUNDS. Note also that we have specified what that value is in the first test.
Naturally, there are many other tests that specify the various aspects of the Card's behavior, and together they turn the user's requirement into an executable specification. That's what Mark 2 is all about.
We should also acknowledge that the tests that we write are not "second class citizens". They require as much love and attention as the production code they specify. This means that after the test has been written we have an opportunity to refactor its design. This is done with respect to specific changes that may be required in the code. These can come from two sources - changing requirements and changing the domain model to reflect changing responsibilities.
Changing requirement could comprise raising the minimal limit or creating a graded discount structure. Changing the domain comprises adding or removing classes or methods on classes.
The customers new requirement is this: there are other ways to charge the user for on-board services. It turns out that guests often do not carry the card with them (to the pool, for example) but would still like to purchase cute drinks with little pink umbrellas. To enable that, a biometric system was installed where the guest can charge the drink to his card by swiping their finger over a fingerprint scanner incorporated into the card reader held by the server.
This means that the model we created where the Card was the central object needs to be refined, and an Account class introduced. The Card is just one way if interacting with the account.
What affect will this have on our test? All reference to Card must be replaced with references to Account. Considering out test code there are two redundancies that we can identify: Card.MINIMAL_FUNDS and card.LoadFunds().
[TestClass]
public class FundLoaderTDD
{
[TestMethod]
public void TestMinimalFunds()
{
Assert.AreEqual(2000, Card.MINIMAL_FUNDS);
}
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
uint minimalFunds = Card.MINIMAL_FUNDS;
Card card = new Card(/*Any card holder's details*/);
card.LoadFunds(minimalFunds);
uint insufficientFunds = minimalFunds - 1;
card = new Card(/*Any card holder's details*/);
card.LoadFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Card.InsufficientFundsException).Name());
}
catch (Card.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
}
We don't like redundancies in our tests any more than we like them in our production code. We extract the redundancies into methods:
[TestClass]
public class FundLoaderTDD
{
[TestMethod]
public void TestMinimalFunds()
{
Assert.AreEqual(2000, MinimalFunds());
}
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
uint minimalFunds = MinimalFunds();
uint insufficientFunds = minimalFunds - 1;
card = new Card(/*Any card holder's details*/);
try
{
LoadFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Card.InsufficientFundsException).Name());
}
catch (Card.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
Card card;
private UInt MinimalFunds()
{
return Card.MINIMAL_FUNDS;
}
private void LoadFunds(uint funds)
{
card.LoadFunds(funds);
}
}
We can inline the MinimalFunds private function and get:
[TestClass]
public class FundLoaderTDD
{
[TestMethod]
public void TestMinimalFunds()
{
Assert.AreEqual(2000, MinimalFunds());
}
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
card = new Card(/*Any card holder's details*/);
LoadFunds(MinimalFunds());
uint insufficientFunds = MinimalFunds() - 1;
card = new Card(/*Any card holder's details*/);
try
{
LoadFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Card.InsufficientFundsException).Name());
}
catch (Card.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
Card card;
private UInt MinimalFunds()
{
return Card.MINIMAL_FUNDS;
}
private void LoadFunds(uint funds)
{
card.LoadFunds(funds);
}
}
Wait! There's another redundancy above:
try
{
//...
Assert.Fail("Card should have thrown a " +
typeof(Card.InsufficientFundsException).Name());
}
catch (Card.InsufficientFundsException exception)
{
//...
}
We are specifying the type of the exception twice...We'll deal with that redundancy in a bit, so we'll put it on the to-do list. In the meanwhile, back to the refactored tests. I do not like the name we gave the LoadFunds method, it's misleading. The customer not want the exception to be thrown every time the card is loaded with a small amount -- only on the initial load. So perhaps this is better:
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
LoadInitialFunds(MinimalFunds());
uint insufficientFunds = MinimalFunds() - 1;
try
{
LoadInitialFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Card.InsufficientFundsException).Name());
}
catch (Card.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
Card card;
private void LoadInitialFunds(uint funds)
{
card = new Card(/*Any card holder's details*/);
card.LoadFunds(funds);
}
Note the fact that card's initialization was moved into the LoadInitialFunds method.
Besides shifting the funds handling responsibility to the Account object, it was also deemed useful to shift the initial amount loading from a specific method to the constructor. So for the $64,000 question - how many places do we need to make this change in? One:
Card card;
private void LoadInitialFunds(uint funds)
{
card = new Card(/*card holder's details*/);
Account account = new Account(funds);
}
And where should the limit be defines? Account, and it will return the value in a method.
private UInt MinimalFunds()
{
return Account.MinimalFunds();
}
Finally, we can make the changes in the test, but only because we left the two references to the exception in the test.
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
LoadInitialFunds(MinimalFunds());
uint insufficientFunds = MinimalFunds() - 1;
try
{
LoadInitialFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Account.InsufficientFundsException).Name());
}
catch (Account.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
The public methods are the specification, the private methods encapsulate implementation. Well, almost, with the exception of the exception handling. But why is an exception being thrown at all?
Well, if you remember, the customer wanted the user to be notified if the amount is too small. Exceptions are just one way of doing it. So we can safely say that the specific exception is an implementation detail, and based on the role we want the public method to play - specification, we really need to get that implementation detail out of here.
So, here's a question to our readers. How would you do it? Note that although we used C# right now, the refactoring principles are relevant to any language.
So without dealing with the exception, yet, this is what the test code looks like.
[TestClass]
public class FundLoaderTDD
{
[TestMethod]
public void TestMinimalFunds()
{
Assert.AreEqual(2000, MinimalFunds());
}
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
LoadInitialFunds(MinimalFunds());
uint insufficientFunds = MinimalFunds() - 1;
try
{
LoadInitialFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Account.InsufficientFundsException).Name());
}
catch (Account.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
private UInt MinimalFunds() {
return Account.MINIMAL_FUNDS;
}
private void LoadFunds(uint funds)
{
Account account = new Account(funds);
}
}
The public methods now essentially constitute an acceptance test. In fact, those familiar with acceptance testing frameworks like FIT would express what these unit test methods communicate in another form, like a table for example, and the private methods would be the fixtures written to connect the tests to the system's implementation.
This does make the test class longer and more verbose, but it also makes it easier to read just the specification part, if that's all you are interested in. Also, when design changes are made later (lets say, for example, that we decide to build the Account in a factory, or store the minimal initial value in a configuration file) that only one private method will be effected by a given change, and none of the public methods at all (which makes sense, since the design has been altered but not the acceptance criteria).
We also have a separation between the specification and implementation. We call these - different perspectives. And they allow us to focus on getting the requirement right, and then getting the design right. We can change the design without affective the requirement.
This is a major piece of making TDD sustainable. As this allows us to change the system design without affecting the public tests which specify the behavior.
So, the $1.000,0000 question is: "Why not write the tests that way to begin with?"
To Be Continued....
It has given us time to think, however. Long airplane rides and such. :)
We're been playing around with an idea we're calling (for the moment) TDD Mark 3 (the notion that TDD is not about testing but rather about specification being TDD Mark 2). To give you an idea of what we're thinking, let's look at an example of TDD Mark 2 as we've been writing tests up to this point, and then refactor it to the TDD Mark 3 style.
Mark 2
So, what is the requirement? Our client is a cruise ship operator. Some of the stuff on the cruise is free and the rest are paid extras. On the ship, the guest can pay for extras with a standard credit card, or with the ship's debit card. Paying with the ship's debit card gives the guest a 5% discount on the purchase cost. The catch is that if you want to use the ship's debit card the guest has to load the card for the first time with at least $2,000. If you try to load a card with less than that amount, the transaction should fail.[TestClass]
public class FundLoaderTDD
{
[TestMethod]
public void TestMinimalFunds()
{
Assert.AreEqual(2000, Card.MINIMAL_FUNDS);
}
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
uint minimalFunds = Card.MINIMAL_FUNDS;
Card card = new Card(/*card holder's details*/);
card.LoadFunds(minimalFunds);
uint insufficientFunds = minimalFunds - 1;
card = new Card(/**/);
try
{
card.LoadFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Card.InsufficientFundsException).Name());
}
catch (Card.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
}
The meaning here is very clear: the LoadFunds() method of Card will throw an InsufficientFundsException if you try to load an amount less than the minimal allowed value. We also show that if the minimal amount is loaded, an exception is not thrown. This constitutes a very typical specification of a behavioral boundary anchored at the value MINIMAL_FUNDS. Note also that we have specified what that value is in the first test.
Naturally, there are many other tests that specify the various aspects of the Card's behavior, and together they turn the user's requirement into an executable specification. That's what Mark 2 is all about.
Refactor to Mark 3
We all know the importance of good design. Good design enables proper code maintainability (more on that in a future blog), which has to do with dealing with change.We should also acknowledge that the tests that we write are not "second class citizens". They require as much love and attention as the production code they specify. This means that after the test has been written we have an opportunity to refactor its design. This is done with respect to specific changes that may be required in the code. These can come from two sources - changing requirements and changing the domain model to reflect changing responsibilities.
Changing requirement could comprise raising the minimal limit or creating a graded discount structure. Changing the domain comprises adding or removing classes or methods on classes.
The customers new requirement is this: there are other ways to charge the user for on-board services. It turns out that guests often do not carry the card with them (to the pool, for example) but would still like to purchase cute drinks with little pink umbrellas. To enable that, a biometric system was installed where the guest can charge the drink to his card by swiping their finger over a fingerprint scanner incorporated into the card reader held by the server.
This means that the model we created where the Card was the central object needs to be refined, and an Account class introduced. The Card is just one way if interacting with the account.
What affect will this have on our test? All reference to Card must be replaced with references to Account. Considering out test code there are two redundancies that we can identify: Card.MINIMAL_FUNDS and card.LoadFunds().
[TestClass]
public class FundLoaderTDD
{
[TestMethod]
public void TestMinimalFunds()
{
Assert.AreEqual(2000, Card.MINIMAL_FUNDS);
}
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
uint minimalFunds = Card.MINIMAL_FUNDS;
Card card = new Card(/*Any card holder's details*/);
card.LoadFunds(minimalFunds);
uint insufficientFunds = minimalFunds - 1;
card = new Card(/*Any card holder's details*/);
try
{card.LoadFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Card.InsufficientFundsException).Name());
}
catch (Card.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
}
We don't like redundancies in our tests any more than we like them in our production code. We extract the redundancies into methods:
[TestClass]
public class FundLoaderTDD
{
[TestMethod]
public void TestMinimalFunds()
{
Assert.AreEqual(2000, MinimalFunds());
}
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
uint minimalFunds = MinimalFunds();
card = new Card(/*Any card holder's details*/);
LoadFunds(minimalFunds);uint insufficientFunds = minimalFunds - 1;
card = new Card(/*Any card holder's details*/);
{
LoadFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Card.InsufficientFundsException).Name());
}
catch (Card.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
Card card;
private UInt MinimalFunds()
{
return Card.MINIMAL_FUNDS;
}
private void LoadFunds(uint funds)
{
card.LoadFunds(funds);
}
}
We can inline the MinimalFunds private function and get:
[TestClass]
public class FundLoaderTDD
{
[TestMethod]
public void TestMinimalFunds()
{
Assert.AreEqual(2000, MinimalFunds());
}
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
card = new Card(/*Any card holder's details*/);
LoadFunds(MinimalFunds());
uint insufficientFunds = MinimalFunds() - 1;
card = new Card(/*Any card holder's details*/);
{
LoadFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Card.InsufficientFundsException).Name());
}
catch (Card.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
Card card;
private UInt MinimalFunds()
{
return Card.MINIMAL_FUNDS;
}
private void LoadFunds(uint funds)
{
card.LoadFunds(funds);
}
}
Wait! There's another redundancy above:
try
{
//...
Assert.Fail("Card should have thrown a " +
typeof(Card.InsufficientFundsException).Name());
}
catch (Card.InsufficientFundsException exception)
{
//...
}
We are specifying the type of the exception twice...We'll deal with that redundancy in a bit, so we'll put it on the to-do list. In the meanwhile, back to the refactored tests. I do not like the name we gave the LoadFunds method, it's misleading. The customer not want the exception to be thrown every time the card is loaded with a small amount -- only on the initial load. So perhaps this is better:
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
LoadInitialFunds(MinimalFunds());
uint insufficientFunds = MinimalFunds() - 1;
try
{
LoadInitialFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Card.InsufficientFundsException).Name());
}
catch (Card.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
Card card;
private void LoadInitialFunds(uint funds)
{
card = new Card(/*Any card holder's details*/);
card.LoadFunds(funds);
}
Note the fact that card's initialization was moved into the LoadInitialFunds method.
Besides shifting the funds handling responsibility to the Account object, it was also deemed useful to shift the initial amount loading from a specific method to the constructor. So for the $64,000 question - how many places do we need to make this change in? One:
private void LoadInitialFunds(uint funds)
{
Account account = new Account(funds);
}
And where should the limit be defines? Account, and it will return the value in a method.
private UInt MinimalFunds()
{
return Account.MinimalFunds();
}
Finally, we can make the changes in the test, but only because we left the two references to the exception in the test.
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
LoadInitialFunds(MinimalFunds());
uint insufficientFunds = MinimalFunds() - 1;
try
{
LoadInitialFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Account.InsufficientFundsException).Name());
}
catch (Account.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
The public methods are the specification, the private methods encapsulate implementation. Well, almost, with the exception of the exception handling. But why is an exception being thrown at all?
Well, if you remember, the customer wanted the user to be notified if the amount is too small. Exceptions are just one way of doing it. So we can safely say that the specific exception is an implementation detail, and based on the role we want the public method to play - specification, we really need to get that implementation detail out of here.
So, here's a question to our readers. How would you do it? Note that although we used C# right now, the refactoring principles are relevant to any language.
So without dealing with the exception, yet, this is what the test code looks like.
[TestClass]
public class FundLoaderTDD
{
[TestMethod]
public void TestMinimalFunds()
{
Assert.AreEqual(2000, MinimalFunds());
}
[TestMethod]
public void TestLoadingLessThanMinimalFundsThrowsException()
{
LoadInitialFunds(MinimalFunds());
uint insufficientFunds = MinimalFunds() - 1;
try
{
LoadInitialFunds(insufficientFunds);
Assert.Fail("Card should have thrown a " +
typeof(Account.InsufficientFundsException).Name());
}
catch (Account.InsufficientFundsException exception)
{
Assert.AreEqual(insufficientFunds, exception.Funds());
}
}
private UInt MinimalFunds() {
return Account.MINIMAL_FUNDS;
}
private void LoadFunds(uint funds)
{
Account account = new Account(funds);
}
}
The public methods now essentially constitute an acceptance test. In fact, those familiar with acceptance testing frameworks like FIT would express what these unit test methods communicate in another form, like a table for example, and the private methods would be the fixtures written to connect the tests to the system's implementation.
This does make the test class longer and more verbose, but it also makes it easier to read just the specification part, if that's all you are interested in. Also, when design changes are made later (lets say, for example, that we decide to build the Account in a factory, or store the minimal initial value in a configuration file) that only one private method will be effected by a given change, and none of the public methods at all (which makes sense, since the design has been altered but not the acceptance criteria).
Mark 3
The separation in perspectives that was created in the above code is a result of refactoring. But it actually makes sense regardless. The public test method is written by intention,and describes the conceptual behavior of the system.We also have a separation between the specification and implementation. We call these - different perspectives. And they allow us to focus on getting the requirement right, and then getting the design right. We can change the design without affective the requirement.
This is a major piece of making TDD sustainable. As this allows us to change the system design without affecting the public tests which specify the behavior.
So, the $1.000,0000 question is: "Why not write the tests that way to begin with?"
To Be Continued....