The bank sample is a small example that serves as a quickstart for NUnit. In this post, we will revisit the sample and see how existing unit tests can be refactored into Pex parameterized tests.
The original unit test:
Let's start with the initial implementation of the Account class, and the first unit test case from the sample:
namespace bank
{
public class Account
{
private float balance;
public void Deposit(float amount)
{
balance+=amount;
}
public void Withdraw(float amount)
{
balance-=amount;
}
public void TransferFunds(Account destination, float amount)
{}
public float Balance
{
get{ return balance;}
}
}
}namespace bank
{
using NUnit.Framework;
[TestFixture]
public class AccountTest
{
[Test]
public void TransferFunds()
{
Account source = new Account();
source.Deposit(200.00F);
Account destination = new Account();
destination.Deposit(150.00F);
source.TransferFunds(destination, 100.00F);
Assert.AreEqual(250.00F, destination.Balance);
Assert.AreEqual(100.00F, source.Balance);
}
}
}
Refactoring the unit test and hooking Pex
An easy oportunity to refactor a unit test into parameterized unit tests is to extract constants as parameters, namely 200F as sourceDeposit, 150F as destinationDeposit, etc... We also add the PexClass and PexTest custom attributes to tell Pex that there are some parameterized tests:
using Microsoft.Pex.Framework;
[TestFixture, PexClass]
public partial class AccountTest
{
[PexTest]
public void TransferFunds(
float sourceDeposit,
float destinationDeposit,
float transfer)
{
Account source = new Account();
source.Deposit(sourceDeposit);
Account destination = new Account();
destination.Deposit(destinationDeposit);
source.TransferFunds(destination, transfer);
Assert.AreEqual(destinationDeposit + transfer, destination.Balance);
Assert.AreEqual(sourceDeposit - transfer, source.Balance);
}
}
Iteration 1: amount should not be negative,
The first (and unique) test case that Pex generates uses float.MinValue for all values, which leads to some interresting floating point issues.
this.TransferFunds(float.MinValue, float.MinValue, float.MinValue);
...
expected: <-Infinity>
but was: <-3.40282347E+38>
at Assert.AreEqual(destinationDeposit + transfer, destination.Balance);
This little test reminds us that floats can have some weird values (infinity, minvalue, etc...).
Iteration 2: amount should not be too big,
It's time to add parameter checking to prevent negative transfer amounts. The ValidateAmount method is added in each transaction method:
private void ValidateAmount(float amount)
{
if (amount < 0)
throw new ArgumentOutOfRangeException("amount");
}
Pex now generates 4 test cases. 3 of them pass negative values as amount to cover the argument validation code:
[Test()]
[ExpectedException(typeof(System.ArgumentOutOfRangeException)), ...]
public void TransferFunds_Single_Single_Single_70408_082415_0_01()
{
this.TransferFunds(float.MinValue, float.MinValue, float.MinValue);
}
...
this.TransferFunds(float.MaxValue, float.MinValue, float.MinValue);
this.TransferFunds(float.MaxValue, float.MaxValue, float.MinValue);
The 4-th test finds another overflow by passing all float.MaxValue.
Iteration 3: NaN handling
We add a MaximumAmount field to bound the amount of money that can be transfered.
private float maximumAmount = 1000.00F;
public float MaximumAmount
{
get{ return maximumAmount;}
}
private void ValidateAmount(float amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException("amount");
if (amount > this.MaximumAmount)
throw new ArgumentOutOfRangeException("amount");
}
We run Pex. The test still does not fail and to our surprise it generates the following passing test:
this.TransferFunds(float.NaN, float.NaN, float.NaN);
Oops, NaN is a very special number. Did you know that (float.NaN == float.Nan) == false? In our case, float.NaN passes the parameter validation and assertions!
Iteration 4: The test fails!
Again, we beef up the 'amount' validation to rule out NaN:
private void ValidateAmount(float amount)
{
if (float.IsNaN(amount))
throw new ArgumentOutOfRangeException("amount");
if (amount <= 0)
throw new ArgumentOutOfRangeException("amount");
if (amount > this.MaximumAmount)
throw new ArgumentOutOfRangeException("amount");
}
We run Pex again and, at last, we find an input to fail the test:
this.TransferFunds(float.Epsilon, float.Epsilon, float.Epsilon);
...
[test] TransferFunds_Single_Single_Single_70408_113456_0_05, AssertionException:
expected: <2.80259693E-45>
but was: <1.40129846E-45>
Iteration 5: Implementing TransferFunds
Now that we found an input that fails the test, we can actually add some code to TransferFunds (as in the QuickStart):
public void TransferFunds(Account destination, float amount)
{
this.ValidateAmount(amount);
destination.Deposit(amount);
Withdraw(amount);
}
So we run Pex again and all the tests passes. Next time, we'll take look at the implementation of the 'MinimumDeposit' feature.