Introduction
BDD (Behavior Driven Development) is a specialization of TDD (Test Driven Development) that encapsulates a set of best practices espoused by the most successful TDD gurus. The main theme is focusing on behavior rather than implementation in your tests. A great tool for verifying behavior (writing BDD-style tests) is RSpec, and now it can be used to test .NET code.
To get more information on BDD, read Dan North's Introduction to BDD. To get some background information on TDD, read Scott Bellware's classic Red-Green-Refactor post or read Kent Beck's TDD By Example (this one's on my to-do list, but I've heard great things about it).
RSpec is a unit testing tool for Ruby that provides an internal DSL which supports nice BDD-style specifications. An example of that syntax is below.
# bowling_spec.rb
require 'bowling'
describe Bowling do
before(:each) do
@bowling = Bowling.new
end
it "should score 0 for gutter game" do
20.times { @bowling.hit(0) }
@bowling.score.should == 0
end
end
RSpec also provides an external DSL (the RSpec Story Runner) that allows you to create executable plain-English feature documentation in the Given/When/Then format typical of BDD tests. This tool has been broken out into its own project called Cucumber, and an example of the syntax is below (from the RSpec website).
Feature: transfer from savings to checking account
As a savings account holder
I want to transfer money from my savings account to my checking account
So that I can get cash easily from an ATM
Scenario: savings account has sufficient funds
Given my savings account balance is $100
And my checking account balance is $10
When I transfer $20 from savings to checking
Then my savings account balance should be $80
And my checking account balance should be $30
Scenario: savings account has insufficient funds
Given my savings account balance is $50
And my checking account balance is $10
When I transfer $60 from savings to checking
Then my savings account balance should be $50
And my checking account balance should be $10
As the RSpec website says "Each Given, When and Then is a Step. The Ands are each the same kind as the previous Step. Steps get defined in Ruby like this (detail left out for brevity) in steps.rb (in the same directory in this example):"
Given /^my (.*) account balance is \$(\d+)$/ do |account_type, amount|
create_account(account_type, amount)
end
When /^I transfer \$(\d+) from (.*) to (.*)$/ do |amount, source_account,
target_account|
get_account(source_account).transfer(amount).to(get_account(target_account))
end
Then /^my (.*) account balance should be \$(\d+)$/ do |account_type, amount|
get_account(account_type).should have_a_balance_of(amount)
end
Getting RSpec and Cucumber set up to work with .NET
The first thing you need to do to start verifying the behavior of .NET code using RSpec is to install IronRuby. In theory, it's also possible to use RSpec to test .NET code by using jRuby and Java's interoperability with .NET, and in fact there's an example of that approach that comes bundled with Cucumber, but I thought this was one too many layers of interoperability (jRuby -> Java -> .NET) for an approach I am going to attempt to use to verify behavior in the majority of code I write. Also, Cucumber's web site says "When IronRuby matures it can be used to 'test' .NET code too", so I took that as a hint that the jRuby way might be problematic in the long run. Finally, I wanted an excuse to play with IronRuby :-) To get the latest version of IronRuby, you must install TortoiseSVN and then do an SVN Checkout from http://ironruby.rubyforge.org/svn/trunk. Open IronRuby.sln in Visual Studio (telling it to "Load Projects Normally" if prompted) and Build Solution. There is a ZIP of IronRuby you can download from rubyforge also, but that didn't work too well for me, so I wouldn't recommend it. (I suspect it's a significantly outdated release.)
In order to obtain Cucumber and all its dependencies, download and install the latest version of the Ruby One-Click Installer (henceforth referred to as regular Ruby). At the command line (from any folder), type "gem Cucumber" and answer "Y" when it asks you to install each of the dependencies. The reason that regular Ruby (rather than IronRuby) is used for this step is because I was not able to get RubyGems to work on IronRuby.
Next, copy the contents of all the gems you just downloaded from C:\ruby\lib\ruby\gems\1.8\gems (assuming you installed regular Ruby to C:\ruby) to C:\Projects\IronRuby\trunk\lib (assuming you checked out the IronRuby trunk to C:\Projects\IronRuby\trunk). A list of the gems you will need to copy is as follows:
cucumber
hoe
polyglot
rake
rubyforge
rspec
term-ansicolor
treetop
Note that you will specifically want to copy only the contents of the "lib" directory of each of these gems. (For example, C:\ruby\lib\ruby\gems\1.8\gems\treetop-1.2.4\lib\treetop.rb will be copied to C:\Projects\IronRuby\trunk\lib\treetop.rb and similarly the C:\ruby\lib\ruby\gems\1.8\gems\treetop-1.2.4\lib\treetop folder will be copied to C:\Projects\IronRuby\trunk\lib\treetop.) Copying the contents of the "lib" folder of each of these gems was the only way I could manage to get IronRuby to recognize all of them at the same time. I have heard that setting the GEM_PATH environment variable to the location where you have put your gems will enable IronRuby to recognize them, but that didn't work for me, which necessitated the kludgy step I just described.
Next, you will need to modify a few files within RSpec and Cucumber to get them to work with IronRuby. the modifications are as follows (paths are relative to C:\Projects\IronRuby\trunk):
- In lib\cucumber\formatters\pretty_formatter.rb, find the "source_comment" method, comment out the body of it, and add simply two double-quotes (an empty string) as the new body of the method. This is because the executable file location doesn't seem to be available in IronRuby. (The commented out code is what would normally print the name of the file and the line number of each code definition referred to by the specification.)
- In lib\cucumber\formatters\ansicolor.rb, comment out the first line that says "gem 'term-ansicolor'". (I wasn't able to get ANSI color to work for the output from Cucumber.)
- In lib\cucumber\tree\step.rb, find the "execute_in" method and then find the following code:
method_line_pos = e.backtrace.index(method_line)
if method_line_pos
strip_pos = method_line_pos - (Pending === e ? PENDING_ADJUSTMENT : REGULAR_ADJUSTMENT)
else
# This happens with rails, because they screw up the backtrace
# before we get here (injecting erb stactrace and such)
end
format_error(strip_pos, proc, e)
if e.backtrace
method_line_pos = e.backtrace.index(method_line)
if method_line_pos
strip_pos = method_line_pos - (Pending === e ? PENDING_ADJUSTMENT : REGULAR_ADJUSTMENT)
else
# This happens with rails, because they screw up the backtrace
# before we get here (injecting erb stactrace and such)
end
format_error(strip_pos, proc, e)
else
e.extra_data = format_error2(proc, e)
raise e
end
- In lib\spec\expectations\errors.rb, add two properties to the "ExpectationNotMetError" class as follows (this step and the last step are necessary because the file name and line are not included with the Exception message in IronRuby as they are in regular Ruby):
def extra_data=(value)
@extra_data = value
end
def message
to_s + "\n" + @extra_data
end
Testing .NET code
To complete the test, I created a simple C# source file to test called "Accent.cs" as follows:
namespace TestLibrary
{
public class Accent
{
private readonly string _stateAbbreviation;
public Accent(string stateAbbreviation)
{
_stateAbbreviation = stateAbbreviation;
}
public string PronounceWord(string word)
{
if (_stateAbbreviation == "MA")
{
switch (word)
{
case "bar":
return "bah";
case "dollar":
return "dolla";
}
}
return word;
}
}
}
I compiled that source file into an assembly called "TestLibrary.dll" and copied it (and TestLibrary.pdb) to a new folder: C:\Projects\IronRuby\trunk\lib\lib.
Next, I created a file called cucumber.yml in C:\Projects\IronRuby\trunk\lib with the following contents (copied from the "calculator" example provided in Cucumber):
default: --format pretty features
I also created another file (also copied from the "calculator" example) called Rakefile with the following contents:
$:.unshift(File.dirname(__FILE__) + '/../../lib')
require 'cucumber/rake/task'
Cucumber::Rake::Task.new do |t|
t.cucumber_opts = "--profile default"
end
Next, I created a file called pronunciation.feature (a specification) in a new folder: C:\Projects\IronRuby\trunk\lib\features with the following contents:
Feature: Pronunciation
In order to gain the trust of a customer
As a sales representative
I want to pronounce words in the dialect of the customer
Scenario: Pronounce a word
Given My client lives in MA
When I pronounce bar
Then the word should be pronounced bah
Scenario: Pronounce a word
Given My client lives in CA
When I pronounce bar
Then the word should be pronounced bar
Scenario: Pronounce a word
Given My client lives in MA
When I pronounce dollar
Then the word should be pronounced dolla
Scenario: Pronounce a word
Given My client lives in CA
When I pronounce dollar
Then the word should be pronounced dollar
Next, I created a file called proncuation_steps.rb (defining the steps in the specification above) in a new folder: C:\Projects\IronRuby\trunk\lib\features\steps with the following contents:
require 'spec'
$:.unshift(File.dirname(__FILE__) + '/../../lib')
require 'mscorlib'
require 'TestLibrary'
Before do
end
After do
end
Given "My client lives in $state" do |state|
@accent = TestLibrary::Accent.new state
end
When /I pronounce (\w+)/ do |word|
@result = @accent.PronounceWord word
end
Then /the word should be pronounced (.*)/ do |result|
@result.to_s.should == result.to_s
end
Finally, I created two files in C:\Projects\IronRuby\trunk\build\debug. The first of them is icuc.rb, which contains the following:
require 'cucumber'
require 'cucumber/cli'
Cucumber::CLI.execute
The second file is icuc.bat, which contains the following:
@echo off
set IRONRUBY=c:\Projects\IronRuby\trunk
pushd %IRONRUBY%\lib
%IRONRUBY%\build\Debug\ir %IRONRUBY%\build\Debug\icuc.rb
popd
set IRONRUBY=
These two files are the equivalent of the "cucumber" and "cucumber.cmd" files in C:\ruby\bin. You can now type "icuc" at a command prompt and it will run the Cucumber test you just created, which should pass! I chose the name "icuc" so that it wouldn't conflict with the "cucumber" command in regular Ruby. Happy testing, er... I mean verifying! ;-)
If you have any trouble getting this to work for you, or if you know of a better way to do it, please leave a comment! I know this method is less than ideal, so I'm hoping one of you can help me improve it. :-)