Plain Text Stories on Rails
October 22nd, 2007
Since my last post on plain text stories, there have already been a few improvements, not the least of which is that it will now work with Rails. Again, this is trunk (rev 2769+) only and experimental.
Here’s a working example from an app that I’m working on:
stories/loginStory: registered user logs in
As a registered user
I want to have to log in
So that only other registered users can see my data
Scenario: user logs in and sees welcome page
Given a user registered with login: foo and password: test
When user logs in with login: foo and password: test
Then user should see the welcome page
Scenario: user logs in with wrong password
Given a user registered with login: foo and password: test
When user logs in with login: foo and password: wrong
Then user should see the login form
And page should include text: There was an error logging in.
Scenario: user logs in with wrong login name
Given a user registered with login: foo and password: test
When user logs in with login: wrong and password: test
Then user should see the login form
And page should include text: There was an error logging in.
[Update: modified to use runner.steps instead of runner.step_matchers]
stories/login.rbrequire File.join(File.dirname(__FILE__), *%w[helper])
run_story :type => RailsStory do |runner|
runner.steps << LoginSteps.new
runner.steps << NavigationSteps.new
runner.load File.expand_path(__FILE__).gsub(".rb","")
end
Here’s what’s new in this example:
- run_story is added to the main object so you don’t have to remember that silly path to the PlainTextStoryRunner which will undoutedbly change!
- run_story accepts arguments, including an options hash, which it will pass to the constructor of the PlainTextStoryRunner (in this case, :type => RailsStory)
- run_story yields the runner, which now supports a load method which you use to tell it where to find the plain text story file.
- run_story … runs the story
Keep your eyes peeled for more updates in the coming days.
Story Runner in Plain English
October 21st, 2007
Houston, we have Plain Text!
I just committed a first stab at a Plain Text Story Runner. It’s in RSpec’s trunk and will be (in some form) part of the next release.
Big thanks to Pat Maddox for the StoryPartFactory (which is now called StoryMediator) and to all on the rspec-users list who contributed their ideas and thoughts to the discussion about plain text stories.
Keep in mind that this is brand new and very experimental. I do not recommend that you start converting all your projects to using this.
That said …
A bit of background
[Update: modified to use And for multiple Givens, Whens or Thens]
The initial implementation of Story Runner supported syntax like this (slightly modified from Dan North’s article introducing 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 do |balance|
@savings_account = Account.new(balance)
end
And "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
And "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
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
end
end
While this is a really cool start, there are a couple of problems. One is that we’re constrained in the way we phrase things. Because the arguments become part of the phrase, we have to structure each phrase so that the argument comes at the end.
The other problem, for me, is that the differing levels of abstraction in the two scenarios make it difficult to read.
Enter Blockless Steps and Parameterized Steps
The first step in resolving this problem was to decouple the expression of the story from the steps, which is accomplished with the use of Parameterized Steps. Here’s how the story above might look:
[Update: using StepGroup/define instead of StepMatchers/add]
steps = StepGroup.new do |define|
define.given("my savings account balance is $balance") do |balance|
@savings_account = Account.new(balance.to_f)
end
define.given("my cash account balance is $balance" do |balance|
@cash_account = Account.new(balance.to_f)
end
define.when("I transfer $amount") do |amount|
@savings_account.transfer_to(@cash_account, amount.to_f)
end
define.then("my savings account balance should be $expected_amount" do |expected_amount|
@savings_account.balance.should == expected_amount.to_f
end
define.then("my cash account balance should be $expected_amount" do |expected_amount|
@cash_account.balance.should == expected_amount.to_f
end
end
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),
:steps => steps do
Scenario "savings account is in credit" do
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"
end
Scenario "savings account is overdrawn" do
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"
end
end
A bit nicer, yes? The steps coming first is a bit noisy, but that could be extracted to another file, or perhaps we can add a means of associating them with the Story after the Story has already been parsed so they can move below the Story.
That bit aside, look how much cleaner the Story reads now. And we can do a couple of additional things to make it even nicer. One thing you might notice is that the line about transfering (When “I transfer 20”) doesn’t specify which way the transfer goes. We can improve on that by enhancing that step:
steps = StepGroup.new do |define|
...
define.when("I transfer $amount from $source to $target") do |amount, source, target|
if source == 'cash' and target == 'savings'
@savings_account.transfer_to(@cash_account, amount.to_f)
elsif source == 'savings' and target == 'cash'
@cash_account.transfer_to(@savings_account, amount.to_f)
else
raise "I don't know how to transfer from #{source} to #{target}"
end
end
...
That lets us write the step as
When "I transfer 20 from savings to cash"
As you can see, this is a big step towards making stories more clear and flexible.
More on Steps
Another thing you may have noticed is that the Steps are grouped together somewhat arbitrarily. Thanks to a couple of handy convenience methods, you can easily build up libraries of these steps and make them as broad or as granular as you like. Perhaps we want the account steps available to many stories, but the transfer step only to this one. Here’s how you can handle that:
class AccountSteps < Spec::Story::StepGroup
steps do |define|
define.given("my savings account balance is $balance") do |balance|
@savings_account = Account.new(balance.to_f)
end
define.given("my cash account balance is $balance" do |balance|
@cash_account = Account.new(balance.to_f)
end
define.then("my savings account balance should be $expected_amount" do |expected_amount|
@savings_account.balance.should == expected_amount.to_f
end
define.then("my cash account balance should be $expected_amount" do |expected_amount|
@cash_account.balance.should == expected_amount.to_f
end
end
end
steps = AccountSteps.new do |define|
define.when("I transfer $amount") do |amount|
@savings_account.transfer_to(@cash_account, amount.to_f)
end
end
Here we’ve created a subclass of StepGroup, instantiated one and defined an additional ‘when’ that will only be available to this instance.
Goodbye quotes!
Once we were able to get rid of the blocks, the quotes made no sense. So we’ve added support for true plain text stories. So now our example can read like this:
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
That gets stored in a plain text file and you can run it by running a ruby file that looks like this:
require 'spec'
require 'path/to/your/library/files'
require 'path/to/file/that/defines/account_steps.rb'
# assumes the other story file is named the same as this file minus ".rb"
runner = Spec::Story::Runner::PlainTextStoryRunner.new(File.expand_path(__FILE__).gsub(".rb",""))
runner.steps << AccountSteps.new
runner.run
And that’s it! It’s that simple. This is still in its very early phases and I’m certain there will be enhancements as people gain experience with it.
If you want to check it out yourself, grab the trunk and do the following:
cd trunk/rspec
ruby examples/stories/calculator.rb
ruby examples/stories/addition.rb
The first example uses Ruby with blockless steps. The second example uses a plain text story stored in examples/stories/addition.
Also, with a couple of small tweaks we’ll be able to consume plain text from any source (not just a local file) and feed it into the PlainTextStoryRunner. This means that we’ll be able to do things like email scenarios to an app that consumes email and runs the scenario against the system and emails you back a report! Crazy, huh?
Lastly, just a reminder, this is only in trunk right now (as of rev 2764), so if you want to explore it you’ll have to get the trunk.
Enjoy!!!!!

