Introducing RBehave
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.
BDD has been around in the Ruby world for a while now, in the form of the excellent RSpec framework, which describes the behaviour of objects at the code level. The RSpec team has focused on creating a simple, elegant syntax and playing nicely with other frameworks, in particular Rails and the Mocha mocking library.
Inspired by this, I wanted to find a simple and elegant way in Ruby to describe behaviour at the application level. This is a different enough problem that I couldn’t just use RSpec. You can skip ahead to the Ruby code if you already know about stories and scenarios. This preamble just sets the scene for the example.
The scenarios that describe a story are made up of “steps” of Givens, Events and Outcomes. These steps can be mixed and matched in different ways to provide different sequences of events. Here is an example:
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
Scenario: savings account is in credit
Given my savings account balance is $100
And my cash account balance is $10
When I transfer $20
Then my savings account balance should be $80
And my cash account balance should be $30
Scenario: savings account is overdrawn
Given my savings account balance is -$20
And my cash account balance is $10
When I transfer $20
Then my savings account balance should be -$20
And my cash account balance should be $10
Here we have two givens: one about my savings account and the other about my cash account. We have a single event, namely transferring cash. We have two outcomes, again about the account balances.
This is typical of the scenarios in a story: they revolve around a single event (the feature itself) and prescribe different outcomes for different combinations of givens. Also, notice that the steps themselves are parameterized: the first time my savings account balance is $100, the second time it is -$20, so a story framework needs to accommodate this.
Getting it running ¶
So then I converted the story into a RSpec-like structure, preferring simple strings to method names, and do/end blocks rather than classes:
require 'rubygems'
require 'rbehave'
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
Given "my cash account balance is", 10
When "I transfer", 20
Then "my savings account balance should be", 80
Then "my cash account balance should be", 30
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
Using RSpec to drive the design, I wrote a little framework that would run these scenarios, each one in its own instance of an object (so they were independent), reusing the steps as needed.
So, this only left the problem of the steps themselves. They would have to be defined somewhere else (that I hadn’t figured out yet). Then I thought: each step’s implementation should be pretty trivial, so what would happen if I put the code for each step inline in the scenario? So I ended up with this:
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
For this example it turns out there are no new steps to define in the second scenario, which makes it very easy to read. In general I’m finding that most of the steps get defined in the first one or two scenarios.
Implementing the code ¶
So I saved this to a file as transfer_funds.rb
and ran it, and I got two failures:
Running 2 scenarios:
FF
2 scenarios: 0 succeeded, 2 failed
FAILURES:
1) transfer to cash account (savings account is in credit) FAILED
NameError: uninitialized constant Account
...
2) transfer to cash account (savings account is overdrawn) FAILED
NameError: uninitialized constant Account
...
RBehave prints one character per scenario: a dot means the scenario passed, an F means it failed. At the end of the run it prints a list of the failing scenarios. So this tells me that firstly it runs (hooray!) and secondly both scenarios are failing because I’m missing an Account class. Well I don’t want a whole bunch of failing scenarios that only start to work one at a time as I implement them. That feels too much like broken windows; I’ll get too used to seeing failing scenarios and then I won’t react when workng scenarios start failing. So I introduced the idea of a “pending” scenario, by adding a pending()
method:
Given "my savings account balance is", 100 do |balance|
pending "needs an Account"
savings_account = Account.new(bal)
end
And I got this:
Running 2 scenarios:
PP
2 scenarios: 0 succeeded, 0 failed, 2 pending
Pending:
1) transfer to cash account (savings account is in credit): needs an Account
2) transfer to cash account (savings account is overdrawn): needs an Account
The Ps represent pending scenarios, which means they aren’t working yet but they don’t count as a failure. Then I use RSpec to implement an Account
with a constructor that takes an initial balance, and give it a transfer_to
method that moves money around.
Then I remove the pending line:
Running 2 scenarios:
.F
2 scenarios: 1 succeeded, 1 failed, 0 pending
FAILURES:
1) transfer to cash account (savings account is overdrawn) FAILED
Spec::Expectations::ExpectationNotMetError: expected -20, got -40 (using ==)
...
Excellent! I have my first working scenario. Now I just need to add behaviour to the Account class to not transfer money it doesn’t have! But wait a minute, what about documentation? Well I added some listeners to the story runner, so when you run:
transfer_funds.rb --dry-run --format simple
you get:
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
Scenario: savings account is in credit
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 80
Then my cash account balance should be 30
Scenario: savings account is overdrawn
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
This is being generated from the same Ruby code that runs the scenarios themselves.
Next steps ¶
You can gem install rbehave
or download the source. There is a sample application (Conway’s Game of Life) in progress in the source code that shows you some of the other features RBehave supports.
RBehave was designed to play nice with other frameworks. In particular, the “world” that each scenario runs in is a module that can be mixed into any object, so you could easily use RBehave with a Rails IntegrationTest
or incorporate it into your existing acceptance testing framework. It is also possible, and in fact encouraged, to use RSpec in your Outcomes (balance.should == 30
).
If you’re interested in using or developing RBehave, please join the mailing lists and let me know how you get on. As a parting thought, RBehave is totally compatible with JRuby, so you could start writing your Java acceptance criteria in Ruby and running them in RBehave.
A number of people helped me get RBehave off the ground. In particular I have to thank Niclas Nilsson for kick-starting the whole thing, David Chelimsky ( RSpec lead) for his sound advice and for adding describe
/it
to the RSpec core, Liz Keogh (JBehave lead) for demanding that steps should take parameters and not taking no for an answer. Also PragDave ran an inspiring Ruby meta-programming workshop at QCon that gave me the courage to try this stuff.