This week I ran a training session at SSW on Unit Testing and Mock Frameworks. Unit testing is very important and the goal of this training session was to highlight that fact. I also tried to show that by doing unit tests first you get:
- more thought into the design and use of your services
- confidence that you are capturing and meeting business requirements
- confidence in changing code because a unit test exists
So to kick things off the session covered creating a very simple banking application that will:
- Allow you to create accounts
- Allow you to deposit money into the account
- Allow you to withdraw money from the account
- Allow you to check the balance of the account
There are some implicit business rules here like you can’t withdraw more money than the account balance.
Step 1 : Domain Objects & Interface Design
We first need to model our domain objects and create interfaces for our different tiers. There are several reasons behind this, the main being to decouple our architecture from the concrete implementation. In other words by using an interface we can easily swap out the implementation of different tiers (e.g. replace an XML persistance store with a SQL persistance store)
Account
This class models an account
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Domain { public class Account { /// <summary> /// Gets or sets the AccountNumber for the account /// </summary> public string AccountNumber { get; set; } /// <summary> /// Gets or sets the Name of the account /// </summary> public string Name { get; set; } /// <summary> /// Gets or sets the BSB of the account /// </summary> public string BSB { get; set; } /// <summary> /// Gets or sets the Pin number of the account /// </summary> public short Pin { get; set; } /// <summary> /// Gets or sets the transactions that have happened to the account /// </summary> public IList<Transaction> Transactions { get; set; } } }
Transaction
This class models a banking transaction
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Domain { public class Transaction { /// <summary> /// Gets or sets the date the transaction occurred /// </summary> public DateTime Date { get; set; } /// <summary> /// Gets or sets the name of the sender /// </summary> public string Sender { get; set; } /// <summary> /// Gets or sets the name of the receiver /// </summary> public string Receiver { get; set; } /// <summary> /// Gets or sets the amount that was transferred in this transaction /// </summary> public double Amount { get; set; } } }
IAccountRepository - The Account Repository
This interface models our persistance layer that will persist accounts.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Domain; namespace BankService.ServiceContracts { public interface IAccountRepository { /// <<summary> /// Gets a list of accounts /// </summary> /// <returns>A list of accounts</returns> IList<Account> GetAccounts(); /// <summary> /// Get a particular account /// </summary> /// <param name="accountNumber">The unique account number of an account</param> /// <param name="pin">The pin number to access the account</param> /// <returns>An account or null</returns> Account GetAccount(string accountNumber, short pin); /// <summary> /// Creates a new account /// </summary> /// <param name="name">The name of the account</param> /// <param name="pin">The pin number to access the account</param> /// <param name="bsb">The bank branch</param> /// <returns>A new account</returns> Account CreateAccount(string name, short pin, string bsb); } }
IBankService - Our friendly bank teller
This interface will describe the facilities offered to the end user
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Domain; namespace BankService.ServiceContracts { public interface IBankService { /// <summary> /// Gets an account /// </summary> /// <param name="accountNumber">The unique account number of an account</param> /// <param name="pin">The pin number to access the account</param> /// <returns>An account or null</returns> Account GetAccount(string accountNumber, short pin); /// <summary> /// Deposit some money into the account /// </summary> /// <param name="accountNumber">The unique account number of an account</param> /// <param name="pin">The pin number to access the account</param> /// <param name="amount">The amount of money to deposit</param> void Deposit(string accountNumber, short pin, double amount); /// <summary> /// Withdraw some money into the account. Will throw and OverdrawnException if the account has insufficient funds /// </summary> /// <param name="accountNumber">The unique account number of an account</param> /// <param name="pin">The pin number to access the account</param> /// <param name="amount">The amount of money to withdraw</param> void Withdraw(string accountNumber, short pin, double amount); /// <summary> /// Gets the current balance of the account /// </summary> /// <param name="accountNumber">The unique account number of an account</param> /// <param name="pin">The pin number to access the account</param> /// <returns>The account balance</returns> double GetAccountBalance(string accountNumber, short pin); /// <summary> /// Transfers money between accounts /// </summary> /// <param name="senderAccountNumber">The account nunmber of the sender</param> /// <param name="recipientAccountNumber">The account number of the receiver</param> /// <param name="amount">The amount of money to transfer</param> void Transfer(string senderAccountNumber, string recipientAccountNumber, double amount); /// <summary> /// Registers a new account /// </summary> /// <param name="name">Name on the account</param> /// <param name="bsb">BSB of the branch the account was registered</param> /// <param name="pin">Pin number used to access the account</param> /// <returns>An account</returns> Account RegisterAccount(string name, string bsb, short pin); } }
Step 2 : Lets start coding!
Hold your horses. Step away from the keyboard. Sit down. Take a few deep breaths and think a little first.
I know it is *very* tempting to jump straight into writing implementations for IAccountRepository and IBankService but to be good developers we need to be a little forward thinking and put some checks and balances in before we start tapping away.
Unit Testing
So what tests do we need to write?
A good starting point would be to look at these rules to better unit testing (which I helped contribute to). In this situation we need to write tests around each operation that modifies an Account because in this scenario bank balances are important. You don’t want to deposit money into your account only to find that your account balance had decreased instead of increased!
In short we need tests around the Withdrawal, Deposit and Account Balance operations
using System; using System.Text; using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using Rhino.Mocks; using BankService.ServiceContracts; using Domain; namespace BankService.Tests { /// <summary> /// This test class will test the functionality of the Bank Service /// </summary> [TestClass] public class BankServiceTests { public BankServiceTests() { } private TestContext testContextInstance; /// <summary> /// Gets or sets the test context which provides /// information about and functionality for the current test run. /// </summary> public TestContext TestContext { get { return testContextInstance; } set { testContextInstance = value; } } /// <summary> /// Tests depositing some money into an account. /// </summary> [TestMethod] public void AccountDepositTest() { IAccountRepository accountRepository = AccountRepository.Instance; BankService bankService = new BankService(accountRepository); // Create an account Account account = bankService.RegisterAccount("Test", "111-111", 1234); // Deposit $1000 into the account bankService.Deposit(account.AccountNumber, account.Pin, 1000.00); double expected = 1000.00; double actual = bankService.GetAccountBalance(account.AccountNumber, account.Pin); Assert.AreEqual(expected, actual); // Deposit another $50.50 into the account bankService.Deposit(account.AccountNumber, account.Pin, 50.50); expected = 1050.50; actual = bankService.GetAccountBalance(account.AccountNumber, account.Pin); Assert.AreEqual(expected, actual); } /// <summary> /// Tests withdrawing money from a valid account with sufficient balance /// </summary> [TestMethod] public void AccountWithdrawTest() { IAccountRepository accountRepository = AccountRepository.Instance; BankService bankService = new BankService(accountRepository); // Create an account Account account = bankService.RegisterAccount("Test", "111-111", 1234); // Deposit some money into the new account bankService.Deposit(account.AccountNumber, account.Pin, 1000.00); // Withdraw some money from the new account bankService.Withdraw(account.AccountNumber, account.Pin, 800.00); double expected = 200.00; double actual = bankService.GetAccountBalance(account.AccountNumber, account.Pin); Assert.AreEqual(expected, actual); } /// <summary> /// Tests the account balance of a valid account /// </summary> [TestMethod] public void AccountBalanceTest() { IAccountRepository accountRepository = AccountRepository.Instance; BankService bankService = new BankService(accountRepository); // Create an account Account account = bankService.RegisterAccount("Test", "111-111", 1234); // Deposit some money into the new account bankService.Deposit(account.AccountNumber, account.Pin, 1000.00); double expected = 1000.00; double actual = bankService.GetAccountBalance(account.AccountNumber, account.Pin); Assert.AreEqual(expected, actual); // Withdraw some money from the new account bankService.Withdraw(account.AccountNumber, account.Pin, 800.00); expected = 200.00; actual = bankService.GetAccountBalance(account.AccountNumber, account.Pin); Assert.AreEqual(expected, actual); } } }