In a previous post, I explained property based testing. In this post we’ll see a simple example in dotnet with Fscheck
Lets assume we built code to transfer money from account A to account B. Some properties that comes to mind are:
- Successful transfer changes account balances.
- Successful transfer leaves source balance as non-negative.
- Failed transfer do not change account balances.
Our first implementation looks like this (don’t judge! … its supposed to be buggy)
class BuggyBank
{
public int BalanceOfAccountA {get; set;}
public int BalanceOfAccountB {get; set;}
public void TranserToAccountB(int Amount)
{
BalanceOfAccountA -= Amount;
BalanceOfAccountB += Amount;
}
}
First we setup an empty dotnet unit test project like this:
mkdir FsCheckExample
cd FsCheckExample
dotnet new mstest
dotnet add package FsCheck --version 3.0.0-beta2
Then we add one test to validate all properties like this. Its vital to use QuickCheckThrowOnFailure()
instead of QuickCheck()
otherwise the test runner never reports failures.
[TestMethod]
public void MultiplePropertiesInOneTest()
{
Prop.ForAll<int, int, int>((StartBalanceForA, StartBalanceForB, AmountToTransfer) => {
BuggyBank T = new()
{
BalanceOfAccountA = StartBalanceForA,
BalanceOfAccountB = StartBalanceForB
};
try
{
T.TranserToAccountB(AmountToTransfer);
}
catch
{
//The transfer failed
bool BalancesUnchanged = (T.BalanceOfAccountA == StartBalanceForA && T.BalanceOfAccountB == StartBalanceForB);
return BalancesUnchanged.Label("Failed transfer do not changes account balances.");
}
//Transfer succeeded
bool BalancesChanged = T.BalanceOfAccountA != StartBalanceForA && T.BalanceOfAccountB != StartBalanceForB;
bool NonNegativeBalanceOfA = T.BalanceOfAccountA >= 0;
return
NonNegativeBalanceOfA.Label("Successful transfer leaves source balance as non-negative")
.And(BalancesChanged).Label("Successful transfer change account balances.")
;
}).QuickCheckThrowOnFailure();
}
When we run this test, FsCheck discovers the code allows transfers of 0 which violate the property that balances must change after successful transfer.
Failed MultiplePropertiesInOneTest [674 ms]
Error Message:
Test method FsCheckExample.FsCheckExample.MultiplePropertiesInOneTest threw exception:
System.Exception: Falsifiable, after 1 test (0 shrinks) (10302727913982160643,12688396740290863899)
Last step was invoked with size of 2 and seed of (10747404355721025928,11972757671557555113):
Label of failing property: Successful transfer change account balances.
Original:
(0, 0, 0)
with exception:
System.Exception: Expected true, got false.
After fixing that bug, FsCheck finds yet another bug! Transfers succeed even when the source account’s balance is insufficient:
Failed MultiplePropertiesInOneTest [444 ms]
Error Message:
Test method FsCheckExample.FsCheckExample.MultiplePropertiesInOneTest threw exception:
System.Exception: Falsifiable, after 8 tests (4 shrinks) (16420426895032412258,4100991820053463935)
Last step was invoked with size of 9 and seed of (13117456436209885594,15897597155289983093):
Labels of failing property (one or more is failing):
Successful transfer change account balances.
Successful transfer leaves source balance as non-negative
Original:
(-2, -2, 2)
Shrunk:
(0, 0, 1)
with exception:
System.Exception: Expected true, got false.