Cucumber and rSpec: Outside-In Development of a Rails Blog

Everyone who's learned Rails (or nearly everyone) has seen the trivial case expressed in terms of creating a simple blog. It's the foo and bar of the Rails world. Not wishing to be different, I'll do exactly the same thing, but from a different perspective. I will describe what the blog should be first, then make it happen. The primary tools I'm using to describe the behaviors are Cucumber and rSpec.

Precursors: Before Starting

This exercise relies on a system configured with Ruby, version 1.8.7 or later, and the following additional Ruby gems.

If Ruby is not installed on your system, go to http://www.ruby-lang.org and follow the instructions there to get it installed on your system. Once you have successfully done this, you should be able to open a terminal window and type:

ruby -v

and the output should be something like:

ruby 1.8.7 (2008-08-11 patchlevel 72) [universal-darwin10.0]

If that's not the case, then consider joining the Ruby Google Group and asking for help with the installation. Also, StackOverflow is a great resource when you are stuck.

Other Tools

Note to Windows Users

This is all written in terms of *nix systems. In most cases, the tools behave exactly the same. However, they are all written in Ruby, so if you find there's a problem (e.g., with script/generate) then try something like ruby script/generate foo.

Behavior Versus Test-Driven Development -- What's the Diff?

The main difference between behavior-driven and test-driven development is that in TDD, one asserts that a given chunk of code has produced a particular result. That's a test. So, e.g.:

def test_foo_is_seven assert_equal(7, @foo) end

It's ok, and it serves the purpose you expected. It verifies the exact function of a particular focused unit of code.

With BDD, however, you specify behaviors couched in terms of what you expect.

describe @foo do
  it "should be seven" do
    @foo.should == 7
  end
end

The readability of specs is higher, and the domain specific language in rSpec allows for a more natural-language specification of expected functionality.

rSpec is a great BDD tool for unit and functional tests, but popping up several thousand feet, you can specify a broader functionality in an integration context. For this, we use a story based tool called Cucumber. Here is an example of how you might describe a story:

Listing 0

Feature: Stopping a Car
  In order to stop my car when I need to
  As a driver
  I want the brakes always to bring the car to a halt

  Scenario: Stopping under normal circumstances
    Given The ignition is on
    And The car is in motion
    When I step on the brake
    Then I should stop

  Scenario: Stopping when the accelerator is also depressed
    Given The ignition is on
    And The accelerator is also depressed
    When I step on the brake
    Then I should stop

  Scenario: Stopping when the accelerator is stuck
    Given The ignition is on
    And The accelerator is stuck
    When I step on the brake
    Then I should stop

This is a very high level (black-box) way to describe what is expected of a given feature rather than the "white box" way we can often test using tools like rSpec.

I've glossed over this stuff very, very quickly, but will start to drill down on it in a moment.

The Obligatory Rails Blog

rails blog
cd blog
script/generate rspec
script/generate cucumber --webrat --rspec
script/generate rspec_scaffold post title:string body:text
rake db:migrate

For the discerning: I specified Webrat when I created the Cucumber scaffold code, but I could have used Capybara to do Selenium integration and test Ajax, Javascript, and browser behaviors more fully.

Open config/environments/test and /cucumber. Code shows that rSpec and Cucumber are hooked up and ready to go!

Let's write a cuke feature called features/posts.feature to show that the home page shows a list of posts.

Listing 1

Feature: Posts
  In order view posts
  As a user
  I want to see a list of interesting posts on the landing page of the site

  Scenario: Going to the landing page of the site
    Given 3 blog posts exist
    When I go to the homepage
    Then I should see "Posts"
    And I should see 3 posts in a table

Note for the adventurous, you can even specify what appears in the posts:

  Scenario: Checking blog posts using tableist
    Given 3 blog posts exist
    When I go to the homepage
    Then I should see "Posts"
    And I should see this table of posts:
      | Title                        | Body                            |
      | This is post number 1        | This is the body text of post 1 |
      | This is post number 2        | This is the body text of post 2 |
      | This is post number 3        | This is the body text of post 3 |

Seems sensible. Really, what you want "in order" to tell you is the business value of the feature. The thinking is that if somehow you cannot answer this in terms of concrete business value, the feature will not be marketable. To do this with blog posts ask:

So this is better written:

In order for people to view posts that tell people
about us so they will become interested enough to 
engage our services and increase our revenue...

We won't make those changes right now.

At the console (i.e., the command line):

rake cucumber

Two undefined steps. Now we have to implement the steps. Note that I haven't fired up a Web browser or server. I know how I want this to behave from the outside, and can verify this via code. Visual inspection is less repeatable. Let's create that verification code that makes sure what should happen is what actually happens.

Create features/step_definitions/posts_steps.rb

Copy/paste the placeholder steps you see on the console.

Modify the first Given to make the number of posts a variable.

Listing 2

Given /^(\d+) blog posts exist$/ do |number_of_posts|
  1.upto(number_of_posts.to_i) do |post_number|
    Post.create(
      :title => "This is post number #{post_number}",
      :body => "This is the body text of post #{post_number}"
    )
  end
end

Things to note: I changed the number 3 to a regular expression placeholder that matches one or more digits. That way, I can specify any number of posts in my scenario. Whoa! Reusability! This is passed into the step, but because it's a regex match, the result is a string, so I explicitly convert it to an integer to control the loop.

rake cucumber

The step still fails. There is no route to '/' which means that visitors to my site will not find this home page! Let's fix it.

Open config/routes.rb

Bang! Fixed it. But now there is still a TODO: I need to figure out whether there are posts in the table.

Go to posts_steps.rb and change the 3 to (\d+).

Listing 3

Then /^I should see (\d+) posts in a table$/ do |number_of_posts|
  response.should have_tag('table') do
    with_tag('tr > td', {:count => number_of_posts.to_i,
                        :text => /body text of post/})
  end
end

This will assure me that the posts are there in the table, and...

rake cucumber

So... why does this work? Because:

  1. Rails scaffold creates a table of items on the index page and I knew that.
  2. I created 3 rows in the database as a Given and I knew what they looked like.
  3. Therefore, there should be exactly 3 rows in the table with the title text I specified.

But how can we really be sure? Change the plain text to ask for 4 rows and see what happens.

Edit posts.feature and change the 3 to 4

rake cucumber

It fails. See the informative error message?

Change it back. Green again.

have_tag and with_tag are rSpec matchers that we are using inside Cucumber. As a side note, they are wrappers of the Test::Unit assert_tag method so the documentation is really under Test::Unit#assert_tag.

What have we learned? That we can specify a high level of behavior with Cucumber without worrying about how the behavior takes place. What could possibly go wrong? Edge cases and regression testing are harder to handle in integration tests like this one. Also, integration tests tend to a couple of attributes that make them a bit less precise in localizing errors:

Let's add the comments feature:

script/generate rspec_scaffold comment body:string

Open CreateComments migration (you'll see the actual filename on the console) and add

t.integer :post_id

so we can create a belongs_to (one to many) relationship between posts and comments.

rake db:migrate

Let's specify that the association exists between posts and comments:

Open post_spec.rb

Listing 4

it "should allow associated comments" do
  post = Post.create(@valid_attributes)
  post.comments << Comment.create(:body => 'OMG, cool!')
  post.comments.should have(1).record
end

Then...

rake

Fails -- undefined comments

Open post.rb and add this at the class level (outside of any method definitions)

has_many :comments

Open comment.rb and add

belongs_to :post

Note that after the migration, the test database may get out of sync with the newest development schema. If this happens, do:

rake db:test:prepare

Now the spec passes, but to make sure, comment out the

post.comments << line

and make sure it fails. Run the focused spec (if you are using TextMate).

Brutal, simple, but now we know how to first spec then code.

How about spec'ing another behavior. The idea of the feature is to reduce spam comments by forcing the user to answer a simple question like "what is 3+4". The sum is placed in pending_answer and the comment is considered spammy. When the user answers it correctly, the answer is placed in supplied_answer and the comment is considered ok. You'll see how much more granular the specs are than a Cucumber scenario of the same kind might have been.

Note that I am driving this particular part right out of rSpec and not Cucumber. Why? Judgement call. I know what the feature is, and there is a scenario that supports it. We'll get to that. But there is a particular behavior I want to focus on first, which is the spam prevention. This could be a case of engineering a solution in the absence of a bigger plan, but as you'll see, it fits nicely in the flow.

Open spec/models/comment_spec.rb

Add this:

Listing 5

describe "to prevent spam" do
  it "should have a pending_answer attribute" do
    Comment.new.should respond_to(:pending_answer)
  end

  it "should have a supplied_answer attribute" do
    Comment.new.should respond_to(:supplied_answer)
  end

  it "should be ok if there is no pending answer" do
    pending
  end

  it "should be ok if pending and supplied answers match" do
    pending
  end

  it "should be spammy if pending and supplied answers fail to match" do
    pending
  end
end

Run it! Note that we now have some failures and some TODO items.

rake

The failures relate to not creating the database columns for the pending and supplied answers. We'll do that now.

script/generate migration add_answers_to_comments

Open the migration and add these:

Listing 6

add_column :comments, :pending_answer, :integer
add_column :comments, :supplied_answer, :integer

Now that you have the migration, run it.

rake db:migrate
rake spec

(If inside TextMate, don't forget rake db:test:prepare)

Time to implement the pending specs. Let's do that:

Listing 7

it "should be ok if there is no pending answer" do
  Comment.create!(@valid_attributes).should be_ok
end

Good a failing spec. Now we need to fix that. Martin Fowler says to start with the simplest implementation that will cause the test to pass, so go to comment.rb:

Add this to the implementation:

Listing 8

def ok?
  pending_answer.blank?
end

Then...

rake

And now this spec passes.

Go to comment_spec.rb

Listing 9

it "should be ok if pending and supplied answers match" do
  Comment.create!(@valid_attributes.merge({:pending_answer => 7, 
                                          :supplied_answer => 7})).should be_ok
end

Note that be_ok takes the predicate method ok? and compares it to true. Another example of rSpec syntactic sugar.

And run it...

rake spec

Fails because we didn't test equality. Let's modify comment.rb to add this to the ok? method. Note that this is actually a refactoring because we are moving from the simplest working solution to a more complete one, backed by red-green-refactor coverage.

pending_answer.blank? || pending_answer == supplied_answer

Ok, now that spec passes. Finally let's handle the spammy case.

Listing 10

it "should be spammy if pending and supplied answers fail to match" do
  Comment.create!(@valid_attributes.merge({:pending_answer => 2, 
                                           :supplied_answer => 9})).
                                           should be_spammy
end

And run it...

rake

Good, another failing spec. Let's fix the code:

Open app/models/comment.rb

Listing 11

def spammy?
  !ok?
end

And...

rake

So what we've done is use rSpec to unit-test this bullet-proof spammer-warder-offer.

Next, let's add a Cucumber feature for the comments that will drive out how the Web app implements not just the model but the whole user experience when commenting (simple though it is).

Add a new feature in features/comments.feature

Listing 12

Feature: Comments
  In order to add comments to a blog post
  As a user
  I want to click on a button on posts and start typing

  Background:
    Given There is a blog post titled "first post, woohoo!"
    And I know the answer to the random number question is 3 and 4

  Scenario: Getting to comments from a blog post
    When I go to the homepage
    And I follow "Show"
    And I follow "comment"
    Then I should see "New comment"

And again, one could ask the why? questions about why comments are important.

Note that the Background sets up "state" that the following scenarios will use. We don't necessarily want to restate the background information in each scenario, so this is a good way to factor it out.

Verifying the results of filling in the answer to the random-number question requires that I already know that answer. To know that means I need to stub out something to return a known value. Let's add a bit of plumbing for using stubs in Cucumber. Open features/support/env.rb and add:

Listing 13

require 'spec/stubs/cucumber'

That tells Cucumber I want to stub something -- include the code. Mocking and stubbing (a topic for a different day) allow me to get a predictable response for an unpredictable event such as a clock read, a network request, or in this case the result of a random number generator. Mocking allows me to set expectations that a particular method on a given object is called and to return particular values.

rake cucumber

Now we have TODOs just like before, so we copy/paste the placeholder into our new file comments_steps.rb. Notice that things like 'press "comment"' are magically provided for me just like "I should see".

Listing 14

Given /^There is a blog post titled "([^\"]*)"$/ do |post_title|
  @post = Post.create!(:title => post_title, :body => "it doesn't matter")
end

Given /^I know the answer to the random number question is (\d+) and (\d+)$/ do |first_number, second_number|
  Kernel.stub!(:rand).and_return(first_number.to_i, second_number.to_i)
end

What I'm saying here is when rand is called, return the first number supplied by the stub, then the second, always in that exact order. A note on "bang" methods. I often use create! instead of create to make my specs fail harder and earlier if I've done something amazingly wrong. The "bang" version tells ActiveRecord to raise an exception rather than simply setting a result code.

First, we need to change some plumbing to make posts and comments resources (in the REST sense). Practically, for this app, this is just following best practices and doesn't buy much. In a larger app using REST can make the architecture more straightforward and easier to scale horizontally. Again, a debate for a different day.

Ok. This is important. If you don't do this, later on your code will be broken and you won't know why. This tells Rails what it needs to know to write URLs like /posts/3/comments/2 and route that to a method with a given set of parameters. Open config/routes.rb and you'll see scaffold versions of these at the top of the file. Change them to reflect the one-to-many relationship we've specified between posts and comments.

Listing 15

# Change at top of routes.rb
map.resources :posts,     :has_many => :comments
map.resources :comments,  :belongs_to => :post

Now add some HTML sauce to views/posts/show.html.erb so someone can navigate to comments. Note the new_post_comment_path? That's the stuff that would have broken if you didn't modify the routes. You did modify the routes, right?

So now we add the new scenario:

Listing 17

Scenario: Adding a comment to a blog post
  When I go to the homepage
  And I follow "Show"
  And I follow "comment"
  And I fill in "insanely great code, dude!" for "Comment"
  And I fill in "7" for "What is the sum of 3 and 4"
  And I press "Create"
  Then I should see "first post, woohoo!"
  And I should see "insanely great code, dude!"

And I run it...

rake cucumber

There is no comment link on the Post show page. We'll fix that by opening views/posts/show.html.erb and adding:

Listing 16

<p><%= link_to 'comment', new_post_comment_path(@post) %></p>

Running Cucumber again reveals that there is no comment field on the form. Let's fix that.

Open views/comments/new.html.erb and add the following for just the form fields. Leave the button there:

Listing 18

<p>
  <%= f.label :body, "Comment" %>
  <%= f.text_field :body %>
</p>
<p>
  <label for="supplied_answer">
    What is the sum of <%= @first_number %> and <%= @second_number %>
  </label>
  <%= t.text_field :supplied_answer %>
</p>      <!-- other stuff that was here before -->

Why the two forms of label? The first (f.label) refers to an ActiveRecord model with an attribute :body. The second refers to a custom field. Rails doesn't help as much here.

Change the back link to link back to the original post in case the user changes his or her mind:

Listing 19

<%= link_to 'Back', post_path(params[:post_id]) %>

Finally, change the form_for so that you can associate the comment with the post:

Listing 20

<% form_for(@comment, :url => post_comments_path(@comment.post)) do |f| %>

We also need to do the following to app/controllers/comments_controller.rb:

Now we want to specify that new creates a comment associated with a post. We need to change the spec to reflect that. To do that, we create an expectation that Post.first will be used to get the comments association, then we stub new on that association to return a new comments object.

Open spec/controllers/comments_controller_spec.rb

Listing 22

describe "GET new" do
  it "assigns a new comment as @comment" do
    # New: Add a mock
    Post.should_receive(:first).any_number_of_times.
                and_return(mock_post({:comments => mock_comment}))
    @mock_comment.stub(:new).and_return(@mock_comment)
    get :new, :post_id => @mock_post
    assigns[:comment].should equal(mock_comment)
  end
end

and add this at the top to create the mock post:

Listing 23

def mock_post(stubs={})
  @mock_post ||= mock_model(Post, stubs)
end

Before running the spec, let's ask what it means? It means that the Post object (which is coupled to the posts table in the database) will be queried using the first method and when that happens, it will send back a fake Post with one fake Comment. That fake Comment actually represents an association proxy in Rails but the only behavior we are interested in is that when it receives the new message, it will return a new Comment object.

Run this spec

rake

And you get the expected failures. Time to fill in the correct behavior:

Listing 21

def new
  @post = Post.first(params[:post_id])
  @comment = @post.comments.new
  @first_number  = Kernel.rand(10)
  @second_number = Kernel.rand(10)
  session[:pending_answer] = @first_number + @second_number

  # other stuff that was already here...
end

One interesting side effect of using scaffolded code is you get specs for things you might not have spec'ed. For example, view behavior. I tend to leave view testing to Cucumber now, but the rspec_scaffold generator provides a complete set of specs that describe the code they generate -- models, views, and controllers. So what happened when we changed the form_for tag to refer to a specific URL that was not the same as the scaffolded default? It broke the view spec. So open the view spec, spec/views/comments/new.html.erb_spec.rb, and fix it up as follows:

Listing 24

it "renders new comment form" do
  post    = create_post
  assigns[:comment] = post.comments.first

  render

  response.should have_tag("form[action=?][method=post]", post_comments_path(post)) do
    with_tag("input#comment_body[name=?]", "comment[body]")
  end
end

What this does is create a post, set the instance variable @comment using the assigns hash, then renders the view. The expectation is that the rendered HTML will have the form tag with the correct action -- post_comments_path(post).

rake

And it passes. Right? Moving right along...

rake cucumber

Ok, now we have a comment field, but we still have a failure. It's because the baked-in Rails behavior is to show the results of a create or edit. What we really want to do is redirect to the original post.

There's a fair amount to do in this next bit, but mostly it's to do with changing the baked-in specs to reflect that comments are associated with posts. Scaffolding is a double-edged sword. Great for getting up and running fast, but there is stuff to undo when you want to make changes.

We want to have create (POST create) only create a comment associated with (scoped to) a post.

So what do we want to have happen when we create a comment:

  1. Add a comment associated with a post
  2. Redirect to the original post show page

Here's the correct POST create ... with valid params example:

Listing 25

it "assigns a newly created comment as @comment" do
  @post = create_post
  post :create, :post_id => @post, :comment => {:body => 'yowzaa!', 
                                                :pending_answer => 7, 
                                                :supplied_answer => 7}
  assigns[:comment].body.should == 'yowzaa!'
end

it "correctly associate a newly created comment as to a post" do
  @post = create_post
  lambda{
  post :create, :post_id => @post, 
       :comment => {:body => 'yowzaa!', :pending_answer => 7, 
                                        :supplied_answer => 7}
  }.should change(@post.comments, :count).by(1)
end

it "redirects to the created comment" do
  @post = create_post
  post :create, :post_id => @post, :comment => {}
  response.should redirect_to(post_url(@post))
end

Remove the invalid params spec because we aren't validating parameters. Really.

Run the specs and they fail miserably. Stay with me. The association code is not there. Also missing is the mysterious new create_post method.

Ok, another BIG topic. Fixture replacement. We mocked and stubbed all sorts of stuff before, but frankly Posts and Comments are simple objects. Setting them up with mocks and stubs is tedious but not impossible. Initially, test fixtures were considered the coin of the realm, and the test database was loaded with some preset data, following which the tests were run. That was great until the schema changed and every test broke. And every piece of fixture data needed to be edited.

So mocks and stubs seemed like a good alternative. They are also blindingly fast because they isolate the code under test from the database abstraction layer and the database itself. The thinking is that when you localize the data closer to the spec it's easier to control how it is created and to modify it should the schema change. It also helps constrain what is being tested to the smallest amount of framework code (or, ideally, no framework code). That's good, but the larger the objects get, the more complex the associations get, the more time you spend constructing the mocks and/or stubs.

Enter fixture replacement. There are several entrants in the Ruby space. My favorites are Machinist and Fixjour. Other people are using ObjectMother and FactoryGirl, to name a couple. The idea is simple. Specify a "blueprint" of an object in one place with sensible default values that should satisfy many specs. Sprinkle in a bit of behavior like randomizing text, etc., and then override only what's necessary.

We'll use Fixjour, which allows your to specify the template for the objects, then build them as needed. Fixjour is a Rubyist's fixture replacement. You may not find the syntax as friendly as Machinist. References: Machinist and Fixjour.

Here is the fixjour_builder for our schema:

Create spec/fixjour_builders.rb

Listing 26

Fixjour do
  define_builder(Post) do |klass, overrides|
    klass.new(
      :title => Faker::Lorem.sentence,
      :body => Faker::Lorem.paragraph,
      :comments => [new_comment]
      )
  end

  define_builder(Comment) {
      :body => Faker::Lorem.sentence,
      :pending_answer => 7,
      :supplied_answer => 7
    }
end

In spec/spec_helper.rb:

Listing 27

require File.expand_path(File.dirname(__FILE__) + "/fixjour_builders.rb")

and

Listing 28

config.include(Fixjour) # This will add the builder
                        # methods to your ExampleGroups
                        # and not pollute Object</code></pre>

In test.rb

Listing 29

config.gem 'fixjour'
config.gem 'faker'

As you can see, we've created builders for Post and Comment. By so doing, we can create an object simply by saying:

@post = new_post # not saved but default values
@comment = create_comment # created and saved with default values
@post = create_post(:title => 'Darn right I overrode the title')

And so on.

Open app/controllers/comments_controller.rb:

In the create method

Listing 31

@comment = Post.first(params[:post_id]).comments.new(params[:comment])
@comment[:pending_answer] = session[:pending_answer]

Listing 32

format.html { redirect_to(post_path(params[:post_id])) }

We'd probably do a redirection to an edit page if the person posting the comment was arithmetic-impaired. But this will suffice for the example.

Run the spec

rake

And it passes. Let's bounce up to get the overall picture again:

rake cucumber

Still a failure because we don't yet have a list of comments on the page. Let's do that:

Open app/models/comment.rb and add a couple of named scopes at class scope outside of any method definitions:

Listing 33

named_scope :ok_comments, :conditions => "pending_answer = supplied_answer"
named_scope :spammy_comments, :conditions => "pending_answer <> supplied_answer"

These are convenient ways to create a query once and use it wherever necessary. Here's an example:

At the end of views/posts/show.html, put:

Listing 34

<ul>
<% @post.comments.ok_comments.each do |comment| %>
  <li><%= comment.body %></li>
<% end %>
</ul>

And now the scenario passes.

The ok? and spammy? methods are still necessary even in the presence of the named scope because they are what you use in the controller when deciding whether to redirect to an edit page so a user can correct his or her addition error. We didn't implement that, but we could have.

But how about a negative scenario? If the comment is spammy?

Listing 35

Scenario: Spamming a comment to a blog post
  When I go to the homepage
  And I follow "Show"
  And I follow "comment"
  And I fill in "insanely great code, dude!" for "Comment"
  And I fill in "10" for "What is the sum of 3 and 4"
  And I press "Create"
  Then I should see "first post, woohoo!"
  And I should not see "insanely great code, dude!"

And when we verify it by...

rake cucumber

And what do you know, the code we spec'ed to detect spam correctly suppresses display of the spammy comment. A reason to preserve spammy comments is that you might want a moderator to go through periodically and look at comments that are flagged as spam to make sure they were not valid comments by an arithmetic-impaired person.