I just read Dan North's article on Introducing RBehave.
In a jealous rage of not being able to utilize Ruby fully in .Net. (YET) I decided to see if I could pull off what Dan had created in rbehave using NUnit. Because I am a huge proponent of reuse, I didn't want to extend my existing Test Coverage with a brand new framework. After all BDD coupled with ReSharper and Visual Studio make for happy testing.
Now before I have the entire TDD community up in arms with what I am about to propose this is merely a pattern for the porting of rbehave to NUnit without having to invest in a whole new framework.
I would like to reiterate Dan's vision of what rbehave is intended to be.
"rbehave is a framework for defining and executing application requirements. Using the vocabulary of behaviour-driven development, you define a feature in terms of a Story with Scenarios that describe how the feature behaves. Using a minimum of syntax (a few “quotes” mostly), this becomes an executable and self-describing requirements document."
Now here is Ruby code for using rbehave:
require ‘rubygems’
require ‘rbehave’
require ’spec’ # for "should" method
require ‘account’ # the actual application code
Story "transfer to cash account",
%(As a savings account holder
I want to transfer money from my savings account
So that I can get cash easily from an ATM) do
Scenario "savings account is in credit" do
Given "my savings account balance is", 100 do |balance|
@savings_account = Account.new(balance)
end
Given "my cash account balance is", 10 do |balance|
@cash_account = Account.new(balance)
end
When "I transfer", 20 do |amount|
@savings_account.transfer_to(@cash_account, amount)
end
Then "my savings account balance should be", 80 do |expected_amount|
@savings_account.balance.should == expected_amount
end
Then "my cash account balance should be", 30 do |expected_amount|
@cash_account.balance.should == expected_amount
end
end
Scenario "savings account is overdrawn" do
Given "my savings account balance is", -20
Given "my cash account balance is", 10
When "I transfer", 20
Then "my savings account balance should be", -20
Then "my cash account balance should be", 10
end
end
So I needed a way to capture the story concept using NUnit. I figured why not use the class text fixture attribute.
| NUnit Syntax | public class Transfer_to_cash_account : NBehaveAbstractFixture
{
/*
As a savings account holder
I want to transfer money from my savings account
So that I can get cash easily from an ATM)
*/
} |
| RBehave Syntax |
Story "transfer to cash account", %(As a savings account holder I want to transfer money from my savings account So that I can get cash easily from an ATM) do |
As you can see the story "transfer to cash account" is the name of the public class Transfer_to_cash_account. As I am sure you all noticed the [TestFixture] attribute is missing but you will also notice that the Transfer_to_cash_account class inherits from NBehaveAbstractFixture, more on this later. Lets talk about the real meat here, the "Given" "When" "Then" constructs.
NUnit Syntax |
[Test]
public void savings_account_is_in_credit()
{
Account savings = null;
Account cash = null;
Given("my savings account balance is", 100,
delegate(int accountBallance)
{
savings = new Account(accountBallance);
});
Given("my cash account balance is", 10,
delegate(int accountBallance)
{
cash = new Account(accountBallance);
});
When("I transfer", 20,
delegate(int transferAmount)
{
savings.TransferTo(cash, transferAmount);
});
Then("my savings account balance should be", 100,
delegate(int expectedBallance)
{
Assert.AreEqual(expectedBallance, savings.Ballance);
});
} |
| RBehave Syntax |
Scenario "savings account is in credit" do Given "my savings account balance is", 100 do |balance| @savings_account = Account.new(balance) end Given "my cash account balance is", 10 do |balance| @cash_account = Account.new(balance) end When "I transfer", 20 do |amount| @savings_account.transfer_to(@cash_account, amount) end Then "my savings account balance should be", 80 do |expected_amount| @savings_account.balance.should == expected_amount end Then "my cash account balance should be", 30 do |expected_amount| @cash_account.balance.should == expected_amount end end |
I have intentionally left the account assertion at 100 so it will generate the following stack trace.
System.ArgumentException:
-Behavior Heap-
Given my savings account balance is: 100
Given my cash account balance is: 10
When I transfer: 20
Then my savings account balance should be: 100
at AlamoCoders.BDD.Domain.Specs.NBehaveAbstractFixture.Then[T](String message, T actionValue, action`1 delegateAction) in NBehaveAbstractFixture.cs:line 59 at AlamoCoders.BDD.Domain.Account_Specs.Transfer_to_cash_account.savings_account_is_in_credit() in RBehave.cs:line 40NUnit.Framework.AssertionException: Expected: 100
But was: 80
The magic is in the anonymous generic delegate. Rather than tell you how this works let me show you. Here is the code for the the NBehaveAbstractFixture.
using System;
using System.Collections.Generic;
using System.Text;
using NUnit.Framework;
namespace AlamoCoders.BDD.Domain.Specs
{
[TestFixture]
public class NBehaveAbstractFixture
{
private string messageHeap;
protected delegate void action<T>(T value);
[SetUp]
protected virtual void SetUp()
{
messageHeap = "\r\n-Behavior Heap-\r\n";
}
[TearDown]
protected virtual void TearDown()
{
messageHeap = string.Empty;
}
protected void When<T>(string message, T actionValue, action<T> delegateAction)
{
try
{
InvokeDelegateAction("When", actionValue, delegateAction, message);
}
catch (Exception e)
{
throw BehaviorException("When", actionValue, e, message);
}
}
protected void Given<T>(string message, T actionValue, action<T> delegateAction)
{
try
{
InvokeDelegateAction("Given", actionValue, delegateAction, message);
}
catch (Exception e)
{
throw BehaviorException("Given", actionValue, e, message);
}
}
protected void Then<T>(string message, T actionValue, action<T> delegateAction)
{
try
{
InvokeDelegateAction("Then", actionValue, delegateAction, message);
}
catch (Exception e)
{
throw BehaviorException("Then", actionValue, e, message);
}
}
private void InvokeDelegateAction<T>(string methodBehavior, T actionValue, action<T> delegateAction, string message)
{
delegateAction(actionValue);
AddMessageToMessageHeap(actionValue, message, methodBehavior);
}
private ArgumentException BehaviorException<T>(string methodBehavior, T actionValue, Exception e, string message)
{
AddMessageToMessageHeap(actionValue, message, methodBehavior);
return new ArgumentException(messageHeap, e);
}
private void AddMessageToMessageHeap<T>(T actionValue, string message, string methodBehavior)
{
messageHeap += string.Format("{0} {1}: {2}\r\n", methodBehavior, message, actionValue);
}
}
}
As you can see this is very crude but it works! Let me know what you think.
Posted
Jun 18 2007, 11:01 AM
by
Joe Ocampo