rspec plain text stories + webrat = chunky bacon!

Motivation for Plain Text Stories

I have been using rspec's new story runner since it was checked into rspec's trunk and have been very pleased with the results. One of the big draws to use the plain text story runner is to allow your customer to help write stories. If you're familiar with XP, these are known as "Customer Stories/Tests" or acceptance tests, and they should be developed by the customer with the help of a developer. Acceptance tests provide a way to easily track progress and tighten the feedback loop between you and the customer. So the idea of customer written tests is nothing new, but the idea of taking the customer written tests and making them executable in their plain text form is something new and exciting that the rspec plain text story runner brings to the table.

Having these customer stories in executable form really helps drive the development process. Many developers, myself included prior to rspec, will develop in a bottom to top fashion by starting with the domain layer and will try to guess what the business logic will need to account for. The stories give you a bird's eye view of a feature from the outside. This helps you concentrate on the actual business value that you're trying to create as you work inwards creating more focused example specs (unit tests) as you go.

Stories in real life

However, one of the challenges I faced was writing reusable steps for the story that translated easily into how the customer would actually like to write the stories. Another big advantage of using the plain text stories is that you can reuse steps on many different stories and potentially different applications. Having customer friendly steps that are also reusable was a bit of a challenge at first when writing stories for a rails app.

Say for example you are creating a website for classifying all the Animals in the world. As such you have your run of the mill animals submission form:< br/>

Animals Submission Form

In my pre-webrat days I would write a story for the submission of an animal like so (the entire code for this project is available below):

Story: Animal Submission

  As a visitor
  I want to add a new animal to the site
  So that I can share my animal knowledge with the community

  Scenario: A visitor successfully submits a new animal
    Given no animal named 'Alligator' exists

    When visitor goes to /animals/new

    Then visitor should see the Animal submission form

    When visitor submits an animal with name: 'Alligator', phylum: 'Chordata', animal class: 'Sauropsida', order: 'Crocodilia', family: 'Alligatoridae', genus: 'Alligator', and lay eggs:  'true'

    Then an animal named 'Alligator' should exist
    And visitor should see the animal show page
    And page should include a notice 'Thank you for your animal submission!'
    And page should have the animal's name, phylum, animal class, order, family, and genus

The steps needed to get these type of tests are as follows:

# animals_steps.rb
steps_for(:animals) do

  Then "$actor should see the Animal submission form" do |actor|
    response.should have_tag("form[action=?][method=post]", animals_path) do
      with_tag("input#animal_name[name=?]", "animal[name]")
      with_tag("select#animal_phylum[name=?]", "animal[phylum]")
      with_tag("input#animal_animal_class[name=?]", "animal[animal_class]")
      with_tag("input#animal_order[name=?]", "animal[order]")
      with_tag("input#animal_family[name=?]", "animal[family]")
      with_tag("input#animal_genus[name=?]", "animal[genus]")
      with_tag("input#animal_species[name=?]", "animal[species]")
      with_tag("input#animal_lay_eggs[name=?]", "animal[lay_eggs]")
    end
  end
end

# navigation_steps.rb
steps_for(:navigation) do

  When "$actor goes to $path" do |actor, path|
    get path
  end

  Then "$actor should see the $resource show page" do |actor, resource|
    response.should render_template("#{resource.gsub(" ","_").pluralize}/show")
  end

  # Pass the params as such: ISBN: '0967539854' and comment: 'I love this book' and rating: '4'
  # this matcher will post to the resource's default create action
  When "$actor submits $a_or_an $resource with $attributes" do |actor, a_or_an, resource, attributes|
    post_via_redirect "/#{resource.downcase.pluralize}", {resource.downcase =&gt; attributes.to_hash_from_story}
  end

  Then "page should include text: $text" do |text|
    response.should have_text(/#{text}/)
  end

  Then "page should include a notice '$notice_message'" do |notice_message|
    response.should have_tag("div.notice", notice_message)
  end

  Then "page should have the $resource's $attributes" do |resource, attributes|
    actual_resource = instantize(resource)
    attributes.split(/, and |, /).each do |attribute|
      response.should have_text(/#{actual_resource.send(attribute.strip.gsub(" ","_"))}/)
    end
  end

end

# database_steps.rb
steps_for(:database) do

  Given "no $resource_class named '$name' exists" do |resource_class, name|
    klass = resource_class.classify.constantize
    klass.destroy_all(:name =&gt; name)
    klass.find_by_name(name).should be_nil
  end

  Then "$a_or_an $resource_class named '$name' should exist" do |a_or_an, resource_class, name|
    resource = resource_class.classify.constantize.find_by_name(name)
    resource.should_not be_nil
    instance_variable_set("@#{resource_class.gsub(" ","_")}", resource)
  end

end

This is okay, but somewhat clunky. The first problem with this approach is that you have to have steps that verify that the correct form exists and then you need steps that artificially submit parameters to the POST url. This makes for brittle stories that are a pain to maintain because you have to update two steps and update the actual story to account for small changes in attributes on the model.

The other problem with this story, and the more important one, is that it is unnatural for the customers to think in terms of posting parameters. They think in terms of forms, clicking buttons, and pretty pictures. The last part of the submit step for the animal stories says: "and lay eggs: 'true'". What does this mean to a customer? Does that mean the user has to select "true" from a select box? Or a radio button option? Or do they have to actually type the word "true"? Additionally, since the story is what should really be driving the entire development process I think it is better to allow the customer to speak in terms of the UI.

The one big pro to the steps above is that they are very reusable, if you notice there is only one custom step for the animals form. But if we really want these to be "customer stories" they need to be able to read better. One way to reach this goal is to write a lot of custom steps for each story. This strategy will give you very pretty stories but will require a lot of work on your part. Since forms are something you tend to deal with a lot in web apps it would be really nice if you could have the best of both worlds; reusable steps and customer friendly stories. Well, it turns out you can!

Enter Webrat

Get the skinny on webrat here: http://www.brynary.com/2007/12/8/webrat-0-1-0-released

Basically... "Webrat lets you quickly write robust and thorough acceptance tests for a Ruby web application. By leveraging the DOM, it can run tests similarly to an in-browser testing solution without the associated performance hit (and browser dependency). The result is tests that are less fragile and more effective at verifying that the app will respond properly to users."

Webrat gives you some very nice methods you can use while doing integration testing in rails apps. You can then easily wrap these methods with some story runner steps:

# webrat_steps.rb
steps_for(:webrat) do

  # note, this is not wrapping all of them...

  When "clicks on '$link'" do |link|
    clicks_link link
  end

  When "fills in $field with '$value'" do |field, value|
    fills_in field, :with =&gt; value
  end  

  When "selects $field as '$option'" do |field, option|
    selects option, :from => field
  end

  When "checks $checkbox" do |checkbox|
    checks checkbox
  end

  When "clicks the $button button" do |button|
    clicks_button button
  end

end

With these new steps our clunky animals story can be rewritten to be much more customer friendly and more maintainable:

Story: Animal Submission

  As a visitor
  I want to add a new animal to the site
  So that I can share my animal knowledge with the community

    Scenario: A visitor successfully submits a new animal
      Given no animal named 'Alligator' exists

      When visitor goes to the home page
      And clicks on 'Create New Animal'

      And fills in Name with 'Alligator'
      And selects Phylum as 'Chordata'
      And fills in Animal Class with 'Sauropsida'
      And fills in Order with 'Crocodilia'
      And fills in Family with 'Alligatoridae'
      And fills in Genus with 'Alligator'
      And checks Lay Eggs
      And clicks the Create button

      Then an animal named 'Alligator' should exist
      And visitor should see the animal show page
      And page should include a notice 'Thank you for your animal submission!'
      And page should have the animal's name, phylum, animal class, order, family, and genus

We can now deal with forms in the language of our stories, something that the customer understands and relates to. This helps a great deal because by just reading the story you now know how the customer wishes the form to be structured. The Phylum is a select box, 'Lays Eggs' is a checkbox, etc... (just like the original form above.) Additionally, we no longer have to test that the form exists because webrat will verify the existence of the form elements and then structure the post parameters accordingly when the form button is "clicked". This leads to tests that are much more robust since they won't break when the names of the input boxes need to be modified due to an attribute renaming on the model (since webrat will look at the label tags), or if the HTTP verb changes for some reason.

So, we get highly reusable steps that are very customer friendly! To help you get started with webrat and rspec's story runner here is the tar file of the animals project that I used in my examples:

UPDATE Please read my post about the different styles of writing stories. The style used in this article is called imperative, and has some disadvantages compared to the other 'declarative' style that you should be aware of. Unless my customer wants otherwise I generally avoid taking a 100% imperative approach.


About this entry