Thursday, March 13, 2008

Story Writing Is Fun

This is about the Story Framework, which is a part of the RSpec library. There's no documentation for the stuff on the official site and very few articles over the Internet, so I think one more example won't hurt, at last it's a really awesome tool.

Generally, Story Writing is a part of BDD, which allows you to define the scope of a system feature along with the criteria of the feature acceptance. If simple and basically, then Stories for BDD are about the same as integration tests for TDD. In case of Rails, this will be RSpec's replacement for the rails native integration tests. The general difference is that it based on the RSpec's expectations. Terrific and fun replacement I should say. 8)

So, lets get down to the deal. My goal for now is to display how a general story should look like and what can you do with it. And because I'm not going to display the stories based development process in action, I'll break the very first rule, precisely "test first". But don't get fooled, in general story writing is a part of BDD process, and in a correct sequence you are writing story first, then you write your specs and code. I just want to show an average story by itself.

Ok, lets assume we have got a rails project, with the rspec and rspec_on_rails plugins installed. Then assume you've generated an Article model rspec_scaffold, then you have installed the restful_authentication plugin and generated the whole user's stuffs. Those are pretty much standard actions, so I won't stop on them here.

First of all you should check your specs and get sure that all of them are happy. Here you should understand the difference between the sepcs and stories. Your specs describes the system objects behavior, this is an equivalent of unit-tests. Stories describes how a whole system (or its aspect) behaves in certain situations. So, writing stories don't forget about your specs, that's your backup and this is not a bad idea to keep the autotest util runned on somewhere in background.

Each story for a first sight might look like something free but they have a particular format. Each story contains one or more scenarios and each scenario describes a particular situation in a simple form setting -> action -> result. Something like that

Story "name", "description" do
Scenario "short descritpion" do
Given "something"
And "something else"
And "maybe something more"

When "I do something"

Then "the system will produce that"
And "it should do that also"
And "probably this one should get happened too"
end

Scenario "..."
end

This is a pretty basic example, just to show common Story structure. As you see this is a piece of ruby code, you calls something, put in it variables and blocks. You even my execute the code if you write it in a file. But now you should notice the three blocks

  • Given [And, And...] - describes the action setting
  • When - describes the action
  • Then [And, And...] - describes the action results

Each of the blocks in really applies additional variables and blocks, the string you passing in is a name of the step. As the story doesn't have any implementation in it, if you have it executed, all the blocks will be marked as "pending".

Ok, this is pretty much it. Lets now write something more meaningful. Say we want that all registered and anonymous users could see the articles index. So we are writing a story.

Story "articles browsing", %{
As a registered or anonymous user
I want to have an access to the articles browsing
So all the people around the world could see the articles
}, :type => RailsStory do

Scenario "anonymous user gets the index" do
Given "an article" do
@article = Article.new
@article.stub!(:valid?).and_return(true)
@article.save
end

When "I get to the page", articles_path do |path|
get path
end

Then "I see the index" do
response.should render_template('index')
end
end

Scenario "registered user gets the index" do
Given "logged in user" do
@user = User.new :login => 'asdf', :password => 'asdf'
@user.stub!(:valid?).and_return(true)
@user.save

post sessions_path :login => @user.login, :password => @user.password
end

And "an article"

When "I get to the page", articles_path

Then "I see the index"
end
end

As you see we are using the RSpec expectations syntactics and doing some activities inside the blocks. You may add more And "..." do; end blocks into your Then description and describe more result criteria you would need to check out. And you can use just the same RSpec tools as inside your controller specs.

More interesting the second scenario, especially this part

And "an article"

When "I get to the page", articles_path

Then "I see the index"

If you have executed the story, you won't see any pending elements, everything will be executed. And there's the tricky point. Each step you fill up with code will be saved inside a story as a method and might later be called by the name you have specified. And sure you can specify arguments for a step. See the case with the When step. In the second call instead of the articles_path could be any path and it would be used inside the block specified in the first scenario but executed in the scope of the second scenario.

Okay, that's nice. But if then we want to write another story, for the "show" action. You don't need to be a master-mind to figure out that this will be a pretty much similar one. Wanna your DRY for the case? No problem, you can extract the steps out of stories at all and create a steps collection. Like that.

class ArticlesAccessSteps < < Spec::Story::StepGroup
steps do |define|
define.given "an article" do
@article = Article.new
@article.stub!(:valid?).and_return(true)
@article.save
end

define.given "logged in user" do
@user = User.new :login => 'asdf', :password => 'asdf'
@user.stub!(:valid?).and_return(true)
@user.save

post sessions_path :login => @user.login, :password => @user.password
end

define.when "I GET the page" do |path|
get path
end

define.then "I see the $page_name" do |page_name|
response.should render_template(page_name)
end
end
end

You might put the code inside the stories/helper.rb file or inside a separated file and require it, or just put above your story and then write your stories like that:

Story "articles browsing", %{
As a registered or anonymous user
I want to have an access to the articles browsing
So all the people around the world could see the articles
}, :type => RailsStory, :steps => ArticlesAccessSteps.new do # <- watch out the ':steps =>' attribute
Scenario "anonymous user gets the index" do
Given "an article"
When "I GET the page", articles_path
Then "I see the index"
end

Scenario "registered user gets the index" do
Given "logged in 'user'"
And "an article"
When "I GET the page", articles_path
Then "I see the index"
end
end

Story "article displaying", :type => RailsStory, :steps => ArticlesAccessSteps.new do
Scenario "anonymous user gets the index" do
Given "an article"
When "I GET the page", article_path(@article)
Then "I see the show"
end

Scenario "registered user gets the index" do
Given "logged in user"
And "an article"
When "I GET the page", article_path(@article)
Then "I see the show"
end
end

Surely it looks much more readable and still will be executed and do what you need. And there's another trick I haven't mentioned. This piece of code

define.then "I see the $page_name" do |page_name|
response.should render_template(page_name)
end

Inside the steps definition. This magic construction "$page_name" will parse out the template name right out of the passed step name and put it into the block. Therefore you can write your steps like

Then "I see the index"
.............................
Then "I see the show"

The same step will be involved, but first time with the 'index' page_name, and the second with the 'show' page_name.

This way you can define your own small macro-language to describe your system behavior with short and easy to understand phrases. You even might show the stuff to your customer and he will understand what's going on and will be sure that the system is doing what he need.

And this is not the end again. As you probably noticed, that after you have moved all the steps code into a standalone class, your story contains nothing but text. In such case you can even go further and put your stories in a plain text. In a such format

Story: articles browsing
As a registered or anonymous user
I want to have an access to the articles browsing
So all the people around the world could see the articles

Scenario: anonymous user gets the index
Given an article
When I GET the page /articles
Then I see the index

Scenario: registered user gets the index
Given logged in user
And an article
When I GET the page /articles
Then I see the index

Isn't that great? You can save that text in a file and then execute it with a simple runner, similar to this one.

require 'rspec'
require 'your_gears' # <- require your necessary files

runner = Spec::Story::Runner::PlainTextStoryRunner.new('your/plain/text/story/file/name')
runner.steps << ArticlesAccessSteps.new
runner.run

You may have one script for all of your textual stories it's up to you. And you'll need to change a little your When step to be like When "I GET the page $path" so it was catching up your urls you specify in the story.

That's it. If you ever written integration tests with the native rails integration tests framework, you probably noticed how ugly and complex they tend to become when the application logic gets serious. So the Story Framework of RSpec offers you an excellent opportunity to change the things for better. With stories you can easily organize your system behavior description in collections of simple scenarios and inside each scenario you can easily set up the necessary conditions, run necessary actions and check the result. And that more important you can do in a very simple way, just with few short strings which anyone can understand.

That's what I wanted to show today.
So long and write the stories it's fun! ;)

2 comments:

Matthias Hennemeyer said...

Hi,
maybe you'll find

this series about speccing steps and DRYing up

interesting.

Matthias

Nikolay V. Nemshilov said...

Yes, right. Thanks for the link.

I run into the dry issues with the stories recently. And it seems like the stories part of rspec is quite raw yet for the point.