Introduction
Unit testing is the process of using short, programmatic tests to test the logic and functionality of discreet units of code. The use of such tests brings a whole host of benefits, not the least facilitating change within the code base (including refactoring), by being able to easily identify if breaking-changes have been introduced; as well as encouraging the use of test-driven development.
NUnit is a well-established, Open-Source unit testing framework for .NET. It is, in my opinion, extremely easy to use, and is very well supported by most good continuous-integration systems, allowing unit tests to be run automatically as part of the build process.
This article is primarily for those of you who are new to unit testing and is intended as a basic introduction to unit testing and test-driven development; as well as how to write basic tests using the NUnit framework.
Our Scenario
The scenario we are going to look at in this article involves a simple BankAccount
class. This class has three methods:
Method | Description |
Deposit() | Deposits money into the account. |
Withdraw() | Withdraws money from the account. Throws an exception if there are insufficient funds to make the withdrawal. |
Transfer() | Transfers money to another account. Throws an exception if there are insufficient funds to make the transfer. |
Our Tests
With this scenario in mind, I have written the following unit tests to test the individual functionality of each of these methods:
[TestFixture]
public sealed class BankAccountTestFixture
{
private BankAccount bankAccountA;
private BankAccount bankAccountB;
[SetUp]
public void SetUp()
{
bankAccountA = new BankAccount(100.00);
bankAccountB = new BankAccount(20.00);
}
[Test]
public void Deposit()
{
bankAccountA.Deposit(10.00);
Assert.That(bankAccountA.Balance, Is.EqualTo(110.00));
}
[Test]
public void Withdraw()
{
bankAccountA.Withdraw(10.00);
Assert.That(bankAccountA.Balance, Is.EqualTo(90.00));
}
[Test]
public void WithdrawWithInsufficentFunds()
{
Assert.That(() => bankAccountB.Transfer(30.00, bankAccountA),
Throws.InstanceOf<OverdrawnException>());
Assert.That(bankAccountA.Balance, Is.EqualTo(100.00));
}
[Test]
public void Transfer()
{
bankAccountA.Transfer(20.00, bankAccountB);
Assert.That(bankAccountA.Balance, Is.EqualTo(80.00));
Assert.That(bankAccountB.Balance, Is.EqualTo(40.00));
}
[Test]
public void TransferWithInsufficientFunds()
{
Assert.That(() => bankAccountB.Transfer(30.00, bankAccountA),
Throws.InstanceOf<OverdrawnException>());
Assert.That(bankAccountA.Balance, Is.EqualTo(100.00));
Assert.That(bankAccountB.Balance, Is.EqualTo(20.00));
}
}
<TestFixture()>
Public NotInheritable Class BankAccountTestFixture
Private _bankAccountA As BankAccount
Private _bankAccountB As BankAccount
<SetUp()>
Public Sub SetUp()
_bankAccountA = New BankAccount(100.0)
_bankAccountB = New BankAccount(20.0)
End Sub
<Test()>
Public Sub Deposit()
_bankAccountA.Deposit(10.0)
Assert.That(_bankAccountA.Balance, [Is].EqualTo(110.0))
End Sub
<Test()>
Public Sub Withdraw()
_bankAccountA.Withdraw(10.0)
Assert.That(_bankAccountA.Balance, [Is].EqualTo(90.0))
End Sub
<Test()>
Public Sub WithdrawWithInsufficientFunds()
Assert.That(Sub() _bankAccountB.Transfer(30.0, _bankAccountA), _
Throws.InstanceOf(Of OverdrawnException)())
Assert.That(_bankAccountA.Balance, [Is].EqualTo(100.0))
End Sub
<Test()>
Public Sub Transfer()
_bankAccountA.Transfer(20.0, _bankAccountB)
Assert.That(_bankAccountA.Balance, [Is].EqualTo(80.0))
Assert.That(_bankAccountB.Balance, [Is].EqualTo(40.0))
End Sub
<Test()>
Public Sub TransferWithInsufficientFunds()
Assert.That(Sub() _bankAccountB.Transfer(30.0, _bankAccountA), _
Throws.InstanceOf(Of OverdrawnException)())
Assert.That(_bankAccountA.Balance, [Is].EqualTo(100.0))
Assert.That(_bankAccountB.Balance, [Is].EqualTo(20.0))
End Sub
End Class
Now let's examine this code a little more closely. Firstly, you will have probably noticed the liberal use of attribute decoration. These attributes serve to identify each of the component parts of our test suite to NUnit. The TestFixture
attribute informs NUnit that the class it is decorating contains one or more tests to be run. The SetUp
attribute identifies a method which is to be executed before each test is run. In our example, we use this to ensure that both our bank accounts always have the same starting balances prior to the execution of each test:
[SetUp]
public void SetUp()
{
bankAccountA = new BankAccount(100.00);
bankAccountB = new BankAccount(20.00);
}
<SetUp()>
Public Sub SetUp()
_bankAccountA = New BankAccount(100.0)
_bankAccountB = New BankAccount(20.0)
End Sub
There is also a corresponding TearDown
attribute which identifies a method which is to be executed after each test is run. Although we don't use this in our example, it is generally used for performing any clean-up operations.
Finally, the Test
attribute identifies each of the individual tests. Let's have a look at one of our tests more closely:
[Test]
public void TransferWithInsufficientFunds()
{
Assert.That(() => bankAccountB.Transfer(30.00, bankAccountA),
Throws.InstanceOf<OverdrawnException>());
Assert.That(bankAccountA.Balance, Is.EqualTo(100.00));
Assert.That(bankAccountB.Balance, Is.EqualTo(20.00));
}
<Test()>
Public Sub TransferWithInsufficientFunds()
Assert.That(Sub() _bankAccountB.Transfer(30.0, _bankAccountA), _
Throws.InstanceOf(Of OverdrawnException)())
Assert.That(_bankAccountA.Balance, [Is].EqualTo(100.0))
Assert.That(_bankAccountB.Balance, [Is].EqualTo(20.0))
End Sub
The Assert.That()
method is used to assert that certain conditions have been satisfied in order to pass the test. In our first assertion, we are asserting that an exception of type OverdrawnException
is thrown when we make a call to the Transfer()
method of the BankAccount
object. The second and third assertions are asserting that the Balance
properties of the two bank accounts are equal to 100.00 and 20.00, respectively.
Running Our Tests (And Fixing Our Code)
So what happens when we run our tests? NUnit provides a simple WinForms app for running unit tests: We simply load our DLL and pick which test(s) we wish to run. The screenshot below shows the output of our tests:
As you can see, three of our five tests fail. This is because, in true test-driven development style, I have written the tests first and I am now only part-way through writing the implementation of the BankAccount
class. Here is the code so far:
public sealed class BankAccount
{
public BankAccount()
: this(0)
{
}
public BankAccount(double initialBalance)
{
Balance = initialBalance;
}
public double Balance { get; private set; }
public void Deposit(double amount)
{
Balance += amount;
}
public void Withdraw(double amount)
{
Balance -= amount;
}
public void Transfer(double amount, BankAccount destination)
{
}
}
Public NotInheritable Class BankAccount
Private _balance As Double
Public Sub New()
Me.New(0)
End Sub
Public Sub New(ByVal initialBalance As Double)
_balance = initialBalance
End Sub
Public ReadOnly Property Balance As Double
Get
Return _balance
End Get
End Property
Public Sub Deposit(ByVal amount As Double)
_balance = _balance + amount
End Sub
Public Sub Withdraw(ByVal amount As Double)
_balance = _balance - amount
End Sub
Public Sub Transfer(ByVal amount As Double, _
ByVal destination As BankAccount)
End Sub
End Class
As you can see, there is no implementation at all for the Transfer()
method. Let's provide one now:
public void Transfer(double amount, BankAccount destination)
{
this.Withdraw(amount);
destination.Deposit(amount);
}
Public Sub Transfer(ByVal amount As Double, ByVal destination As BankAccount)
Me.Withdraw(amount)
destination.Deposit(amount)
End Sub
If we re-run the tests, we can now see the Transfer()
test now passes:
However, our tests are still failing when we have insufficient funds in our account to make either a withdrawal or a transfer. A simple modification to our Withdraw()
method should now fix both of these:
public void Withdraw(double amount)
{
if (Balance >= amount)
Balance -= amount;
else
throw new OverdrawnException(
"You have insufficient funds in the account.");
}
Public Sub Withdraw(ByVal amount As Double)
If _balance >= amount Then
_balance = _balance - amount
Else
Throw New OverdrawnException(_
"You have insufficient funds in the account.")
End If
End Sub
Now, all our tests should be green when we run them again:
Summary
Unit testing is a highly useful and important tool when working on development projects of all sizes. It provides a degree of confidence that the code being developed is fit-for-purpose and does not break any existing functionality of the system.
With the NUnit framework, it is very simple to write unit tests for test-driven development scenarios.
Further Reading