Introduction
During my exploration of BDD frameworks for .NET, I only had one final runner-up as a BDD framework: Machine.Specifications. This is a very nice framework, but in my quest for the holy grail on BDD, it got me started on thinking about an even better BDD framework. In this article, I will layout my initial ideas about a BDD framework named NetSpec.
Likes and Dislikes about MSpec
MSpec is in my personal opinion one of the best BDD frameworks available for .NET, due to the very nice syntax.
An Example
First I'll show you the Account
class:
public class Account
{
public Decimal Balance {get;set;}
public void Transfer(decimal amount,Account ToAccount)
{
if (Balance < amount)
{
throw new ArgumentOutOfRangeException
("There is not enough money available on the account");
}
Balance-=amount;
ToAccount.Balance+=amount;
}
}
Next up, I will show you the specs in Mspec:
using System;
namespace Machine.Specifications.Example
{
[Subject(typeof(Account), "Funds transfer")]
public class when_transferring_between_two_accounts
: with_from_account_and_to_account
{
Because of = () =>
fromAccount.Transfer(1m, toAccount);
It should_debit_the_from_account_by_the_amount_transferred = () =>
fromAccount.Balance.ShouldEqual(0m);
It should_credit_the_to_account_by_the_amount_transferred = () =>
toAccount.Balance.ShouldEqual(2m);
}
[Subject(typeof(Account), "Funds transfer"), Tags("failure")]
public class when_transferring_an_amount_larger_than_the_balance_of_the_from_account
: with_from_account_and_to_account
{
static Exception exception;
Because of =()=>
exception = Catch.Exception(()=>fromAccount.Transfer(2m, toAccount));
It should_not_allow_the_transfer =()=>
exception.ShouldBeOfType<exception />();
}
public class failure {}
public abstract class with_from_account_and_to_account
{
protected static Account fromAccount;
protected static Account toAccount;
Establish context =()=>
{
fromAccount = new Account {Balance = 1m};
toAccount = new Account {Balance = 1m};
};
}
}
While this is a very keen and intelligent approach, there are a few things that I am not very fond of:
- The "
Subject
" attribute: is really code litter, and not really acceptable IMHO - The fact that we use classes and subclasses for each scenario; sometimes this becomes too much of a hassle; your code gets split up into several places, and this is not very transparent anymore...
- The lack of Story support
- The catching of expected exceptions should not need a
try catch
block but have some kind of other mechanism - The fact that the context is intermixed with the class itself => this might be a subject of discussion, but it is certainly not my personal preference; I prefer a separate context class per story and/or scenario
- Some definitions, i.e. "because of", "Establish","It" etc... are not good enough in my opinion
- The use of underscores; I am more of a PascalCasing fan
Based on my ideas, I created a new BDD Framework: NetSpec.
The NetSpec Approach
Based on my findings, I started coding this morning, and this is the spec defined in NetSpec:
class TransferFundsBetweenAccounts : Story
{
AsAn AccountUser;
IWant ToTransferMoneyBetweenAccounts;
SoThat ICanHaveRealUseForMyMoney;
class Transfer1MBetweenTwoAccounts : Scenario<Context>
{
Given BothAccountsHave1M = () => new Context(1m,1m);
When Transfering1MFromAToB = c => c.AccountA.Transfer(1m,c.AccountB);
ItShould Have0OnAccountA = c => c.AccountA.Balance.ShouldEqual(0);
ItShould Have2mOnAccountB = c => c.AccountB.Balance.ShouldEqual(2m);
}
class TransferTooMuch : Scenario<Context>
{
Given BothAccountsHave1M = () => new Context(1m, 1m);
When Transfering2MFromAToB = c => c.AccountA.Transfer(2m,c.AccountB);
ItShould Have1mOnAccountA = c => c.AccountA.Balance.ShouldEqual(1m);
ItShould Have1mOnAccountB = c => c.AccountB.Balance.ShouldEqual(1m);
ItShouldThrowException<ArgumentOutOfRangeException> AmountToHigh;
}
public class Context
{
public Context(decimal amounta, decimal amountb)
{
AccountA.Balance = amounta;
AccountB.Balance = amountb;
}
public Account AccountA = new Account();
public Account AccountB = new Account();
}
}
Now, I do not know what you think, but in my opinion this is a lot more readable then the MSpec version.
While the code currently compiles, the testing itself is not implemented yet, but this should not be such a big hurdle. The automatic catching of the exception should only happen in the "When"-phase afaik, other exceptions should ALWAYS return a fail.
Conclusion
In my quest for the holy grail, I have met a lot of hurdles but I do think I am getting closer to my target. I hope/think that a domain expert should be able to read the code I present in the specifications; In my opinion this should be a viable DSL-like human-readable text. While the current representation still has some constraints due to its implementation language, most of the disturbing stuff has been left out.
Up next is the implementation of the testrunner, but I will have to take a look at my schedule when I will be able to complete this, since I sometimes have to do some work that pays the bills as well ;) ...
If you have any suggestions or remarks, please do let me know in the comment section !!