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:

// C#
[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));
    }
}
VB.NET
' Visual Basic
<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:

// C#
[SetUp]
public void SetUp()
{
    bankAccountA = new BankAccount(100.00);
    bankAccountB = new BankAccount(20.00);
}
VB.NET
' Visual Basic
<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:

// C#
[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));
}
VB.NET
' Visual Basic
<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:

// C#
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)
    {
        //TODO:  Do some stuff here.
    }
}
VB.NET
' Visual Basic
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)
        ' TODO: Do some stuff here.
    End Sub
End Class

As you can see, there is no implementation at all for the Transfer() method. Let's provide one now:

// C#
public void Transfer(double amount, BankAccount destination)
{
    this.Withdraw(amount);
    destination.Deposit(amount);
}
VB.NET
' Visual Basic
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:

// C#
public void Withdraw(double amount)
{
    if (Balance >= amount)
        Balance -= amount;
    else
        throw new OverdrawnException(
          "You have insufficient funds in the account.");
}
VB.NET
' Visual Basic
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

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"