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) doScenario "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 ATMScenario: 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!!!!!


October 21st, 2007 at 8:24 pm
Neat. Still hate the reinforcement of capturing/duplicating user stories in programmer artifacts, though.
October 21st, 2007 at 8:24 pm
Wow, that is utterly cool, thanks!
October 21st, 2007 at 8:24 pm
Very cool David. I haven’t had a chance to play with the story aspect of rspec too much; this makes me even more interested.
October 21st, 2007 at 8:24 pm
This is REALLY great stuff David.
October 21st, 2007 at 8:24 pm
Excellent work, David. I think you did a remarkable job of integrating so much diverse feedback from different people on the mailing list.
October 21st, 2007 at 8:24 pm
Awesome, that looks great. I’m sure to start using it regularly now. Interested to see what (if any) improvements can be made to it in future.
October 21st, 2007 at 8:24 pm
Great stuff David! I’ve only been tinkering with Story Runner, but this looks like an excellent step forward.
October 21st, 2007 at 8:24 pm
Nice piece of work. Back a while ago when setting PHPSpec’s scope there was brief chatter about plain text parsing and such. It’s great to see this functionality in rspec to work with. All we need now are methods of storing and transporting such plain text between rspec and whichever project management system is in use ;).
October 21st, 2007 at 8:24 pm
Wincent, perhaps a next level is a story builder app which uses option lists and/or autocompletion for adding steps to a scenario from the existing ones, and lets the customer create new step clauses but validates syntax constraints.
October 21st, 2007 at 8:24 pm
VERY glad to see the expressions pulled out of the steps. A little worried about the plain text though. To me it seems to de-emphasize the fact that you’re essentially still writing “code” that needs to match a very specific syntax. Not sure if that’s good or bad…
October 21st, 2007 at 8:24 pm
re: concerns about customers living within the constraints of syntax:
October 21st, 2007 at 8:24 pm
great work David (and everyone contributing + discussing rspec + rbehave)
- Is the context of a Rails project, is the story runner format used for all levels of testing (models, controllers, integration) -or just @ the integration level?October 21st, 2007 at 8:24 pm
Better phrasing on question: In a Rails project, CAN the story runner format be used
all levels of testing, or is it practical onlyintegration level. I just love the idea of plain-text test descriptions.October 21st, 2007 at 8:24 pm
Hi Ryan,
October 21st, 2007 at 8:24 pm
Great stuff! Many thanks
October 21st, 2007 at 8:24 pm
This is wonderful! It solves my biggest concern about the new rspec story runner.
October 21st, 2007 at 8:24 pm
Great Stuff Dave. Thanks. Agree with Justin about pulling out the plain text part though. Do we need to go to that extent, since it is a programmer’s tool anyway.
October 21st, 2007 at 8:24 pm
OK I give, there is no way we are going to be able to match this awesomeness in C# on the NBehave project.
October 21st, 2007 at 8:24 pm
This is fantastic. I watched your Story Runner presentation / lightning talk at Rails Conf Europe and was instantly convinced that this is of real value for acceptance / integration test things.
October 21st, 2007 at 8:24 pm
The plaintext format is interesting insofar as it makes tests more readable for the customer once written, but I still think that asking a lay user to authored acceptance tests is like asking a baby to change its own diaper. One way or another you’re going to have to lend a hand or get shit all over the place.
October 21st, 2007 at 8:24 pm
@benjamin_jackson: the goal is not to have customers write stories in a vacuum. The goal is to be able to collaborate on stories with customers and to then provide customers and testers the tools to use the stories as an exploratory tool.
October 21st, 2007 at 8:24 pm
Great work. It’s entertaining and informative. Thanks for the info!