BlackWaspTM

This web site uses cookies. By using the site you accept the cookie policy.This message is for compliance with the UK ICO law.

Programming Concepts
.NET 3.5+

Test-Driven Development

Test-driven development is a process that assists in creating high quality, well designed, loosely coupled and maintainable code that can be refactored with confidence. The process relies on writing unit tests before creating the code that they validate.

Iteration Four

For the final two iterations we'll deal with the transaction history for an account. Our first test simply confirms that a newly created Account object contains no history.

[Test]
public void NewAccountsHaveNoHistory()
{
    Account account = new Account();

    CollectionAssert.IsEmpty(account.History);
}

To make the code compile we need to add the History property to the Account class. We don't want the history to be editable so we'll return an IEnumerable<Transaction> sequence, rather than another type of list or collection.

public IEnumerable<Transaction> History { get; private set; }

When you run the tests you'll see a failure because the sequence is never initialised. Let's set it to be a new List<Transaction> in the Account constructor:

public Account()
{
    History = new List<Transaction>();
}

Our tests are now all green. No refactoring is required at this point.

Iteration Five

The fifth iteration will be the final one in this example. Here we'll check that transactions for both deposits and withdrawals are recorded in the History property. The test checks that the values and descriptions are stored in the correct order:

[Test]
public void TransactionsAppearInHistory()
{
    Account account = new Account();
    Deposit deposit = new Deposit(100M, "Opening Balance");
    Withdrawal withdrawal = new Withdrawal(23.45M, "Withdrawal");

    account.RecordTransaction(deposit);
    account.RecordTransaction(withdrawal);

    decimal[] values = new decimal[] { 100M, -23.45M };
    CollectionAssert.AreEqual(values, account.History.Select(t => t.Value));

    string[] descs = new string[]
    {
        "Opening Balance",
        "Withdrawal"
    };
    CollectionAssert.AreEqual(descs, account.History.Select(t => t.Description));
}

As we aren't storing the transactions yet this test fails. To make it pass we need to update the RecordTransaction method to add the transaction to the list. This is achieved, in a rather ugly way, in the following updated method. Despite being poor code, it is acceptable at this stage.

public void RecordTransaction(Transaction trx)
{
    Balance += trx.Value;
    ((List<Transaction>)History).Add(trx);
}

With the test passing we know that the code is functioning correctly. We can now refactor to remove the cast operation. We'll add a private field for the list and update the History property to return it. In the final Account class below I've also used LINQ's Select operator when generating the transaction history sequence. This will prevent consumers of the Account from casting the returned value to a list and modifying its contents.

public class Account
{
    List<Transaction> _history;

    public IEnumerable<Transaction> History
    {
        get { return _history.Select(t => t); }
    }

    public decimal Balance { get; private set; }

    public Account()
    {
        _history = new List<Transaction>();
    }

    public void RecordTransaction(Transaction trx)
    {
        Balance += trx.Value;
        _history.Add(trx);
    }
}

We could go further with this example by adding tests and code that validates the values and descriptions for deposits and withdrawals, checks that accounts can't go overdrawn, etc. However, hopefully the five iterations give a good idea of the process and the thought processes. It may also have shown how a good design can emerge, where other processes that do not include the refactoring phases may lead to less pleasing code.

19 November 2012