//
you're reading...
Cucumber, MongoDB

Tutorial: Using Cucumber testing with MongoDB

This example is based on the following tutorial http://www.mongodb.org/display/DOCS/MongoDB+Data+Modeling+and+Rails. I recommend reading it to get familiar with how MongoDB is integrated with a Rails application.

I rewrote the project using the TDD approach with Cucumber and the source code can be found on github

1. First of all, create a mock application with suspenders.

gem install suspenders
suspenders create mongo-story-mock

We will use this application later to copy over the Cucumber tests and other content generated by Clearance. We cannot generate this content directly for our MongoDB application, because it will fail without ActiveRecord which we don’t have in our case.

Now, let’s create our application:

rails new mongo-story --skip-active-record
cd mongo-story
rails generate cucumber:install
rails generate rspec:install
rails generate formtastic:install
rails generate clerance:views

The first command will create a project with no migration files for ActiveRecord. Then we create folders for Cucumber features and rspec, and initialize formtastic and clearance views.

Make sure, that the Gemfile contains all the gems from the source code, and those are installed. Also copy over from the source code config/inializers mongo_mapper_ext.rb and plucky_ext.rb, those are patches to make sure the testing runs.

Now, we return back to our mock project, and copy over the features/clearance folder to our features folder, and features/step_definitions/clearance folder to the feature/step_definitions folder.

Also, insert the following cases into features/support.paths.rb, path_to method:

 
case page_name
...
 when /the sign up page/i
     sign_up_path
 when /the sign in page/i
     sign_in_path
 when /the password reset request page/i
     new_password_path

We can also copy the generated views, i.e. copy the app/views/users folder to the app/views. I was never able to generate proper clearance views (may be, it’s fixed now), so make sure that your view files look the following:

app/views/users/new.html.erb
----------------------------
<h2>Sign up</h2>

<%= semantic_form_for @user do |form| %>
<%= render :partial => '/users/form', :object => form %>
<%= form.submit 'Sign up' %>
<% end %>

app/views/users/_form.html.erb
-------------------------------
<%= form.error_messages %>
<%= render :partial => '/users/inputs', :locals => {:form => form} %>

app/views/users/_inputs.html.erb>
--------------------------------
<%= form.inputs do %>
<%= form.input :username, :label => "User name" %>
<%= form.input :email, :as => :email %>
<%= form.input :password %>
<%= form.input :password_confirmation, :label => "Confirm password" %>
<% end %>

Also, copy over the controller for users from app/controllers/users_controller.rb and the user model from app/models/user.rb, and also the user routing information. Remove inheritance from ActiveRecord in user.rb.

Now, we can try to run the clearance tests as bundle exec cucumber and see them fail, because the model has no fields. To fix that, we will add MongoMapper keys, such as our user.rb will look the following:

class User
  include MongoMapper::Document
  include Clearance::User
  
  key :username, String
  key :email, String
  key :encrypted_password, String, :limit => 128
  key :salt, String, :limit => 128
  key :confirmation_token, String, :limit => 128
  key :remember_token, String,:limit => 128
  timestamps!
  
end

For the user model, we also want username and email to be non-empty. In order to ensure that, we create an rspec test:

spec/models/user_spec.rb
------------------------
require 'spec_helper'

describe User, 'valid' do
  it { should validate_presence_of :username }
  it { should validate_presence_of :email }
end

Run the test as bundle exec rspec spec/models/user_spec.rb and see it fail because we didn’t specify those keys are required. Fix it in the user.rb model, adding the required attribute:

  key :username, String, :required => true
  key :email, String, :required => true

Now, the rspec test should pass. Go back to Cucumber tests, and check if they pass. I leave finishing it as an exercise.

2. Now, let’s create some Cucumber features for the Story. Create the following feature:

features/visitor_creates_story.feature
--------------------------------------
Feature: Create a story
  As a user
  I can write stories
  So that other visitors can read them

Scenario: user creates a story
  Given I have signed in with "me@example.com/test"
  When I go to the new story page
  And I publish a story with "My story" and "http://www.mysite.com/story1"
  Then I should be on the story page for "My story"
  And I should see "You have published a story"

Scenario: user tries to create a story with an empty title and empty url
  Given I have signed in with "me@example.com/test" 
  When I go to the new story page
  And I publish a story with "" and ""
  Then I should see error messages
  

It’s going to complaint that there is no step definition such as “I publish a story…”. Add the following file features/step_definitions/story_steps.rb:

require 'uri'
require 'cgi'
require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths"))
require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "selectors"))

module WithinHelpers
  def with_scope(locator)
    locator ? within(*selector_for(locator)) { yield } : yield
  end
end
World(WithinHelpers)

When /^(?:|I )publish a story (?:with|as) "([^"]*)" and "([^"]*)"$/ do |title, url|
  fill_in "Story title", :with => title
  fill_in "Url", :with => url
  click_button "Post"
end

Run it again, it fails the second line of the first scenario because there is no Story functionality in the project. We proceed in the usual way with Cucumber, creating the StoriesController with a method “new”, the stories/new.html.erb view and the story.rb model. For the controller, please, refer to the rspec tests in spec/controllers/stories_controller_spec.rb for the routing conditions, and make sure they pass. I will concentrate here on the Story model. Create the model as the following:

app/models/story.rb
----------------------------------
class Story
  include MongoMapper::Document
  
  key :title,     String
  key :url,       String
  key :slug,      String
  key :voters,    Array
  key :votes,     Integer, :default => 0
  key :relevance, Integer, :default => 0

  # Cached values.
  key :comment_count, Integer, :default => 0
  key :username,      String

  # Note this: ids are of class ObjectId.
  key :user_id,   ObjectId, :required => true
  timestamps!

end

With this model, we want to make title and url fields compulsory, validate the url, and also story should belong to user. Create the spec/models/story_spec.rb file to make sure those conditions are met.

spec/models/story_spec.rb
-------------------------
require 'spec_helper'

describe Story, 'valid' do
  it { should belong_to :user }
  it { should validate_presence_of :user_id }
  it { should validate_presence_of :title }
  it { should validate_presence_of :url}
  it { should validate_format_of(:url).with($URL_REGEX)}
end

It will fails when run as bundle exec rspec spec/models/story_model.rb. To make it pass, add the conditions to the story.rb.

class Story
  include MongoMapper::Document
  
  key :title,     String, :required => true
  key :url,       String, :required => true
  key :slug,      String
  key :voters,    Array
  key :votes,     Integer, :default => 0
  key :relevance, Integer, :default => 0

  # Cached values.
  key :comment_count, Integer, :default => 0
  key :username,      String

  # Note this: ids are of class ObjectId.
  key :user_id,   ObjectId, :required => true
  timestamps!
# Relationships. belongs_to :user validates_format_of :url, :with => $URL_REGEX
end

Now the rspec will pass. I leave finishing the feature as an exercise. Please, pay attention to the authentication requirement, such as a user should be logged in when creating a story. Those conditions are described in spec/controllers/stories_controller_spec.rb. We also need to define a user factory in spec/factories/user.rb with FactoryGirl to make the authentication spec pass.

3. Now, let’s try to create scenarios involving upvoting with AJAX. Let’s create the following feature features/visitor_views_story_list.feature:

Feature: story list
   In order to see what stories are available and upvote
   As a visitor
   I can see a list of stories

Background:
   Given the following stories exist:
       | title         |  url                           | votes  | user                       |
       | My story      |  http://www.writer1.com/story1 | 5      | email: writer1@example.com |
       | My best story |  http://www.writer2.com/story1 | 10     | email: writer2@example.com |

Scenario: visitor views story list
   When I go to the stories page
   Then I should see a link "My story" to "http://www.writer1.com/story1"
   And I should see "6 votes"
   And I should see a link "My best story" to "http://www.writer2.com/story1"
   And I should see "11 votes"
   And I should see a link "Sign in"
   And I should not see an upvoting link

Scenario: registered user views story list with stories they haven't upvoted
   Given I have signed in with "me@example.com/test"
   When I go to the stories page
   Then I should see a link "My story" to "http://www.writer1.com/story1"
   And I should see an upvoting link for "My story"
   And I should see "6 votes"
   And I should see a link "My best story" to "http://www.writer2.com/story1"
   And I should see an upvoting link for "My best story"
   And I should see "11 votes"
   And I should not see a link "Sign in"

@javascript
Scenario: registered user views story list with stories they have upvoted
   Given I have signed in with "me@example.com/test"
   And I have upvoted on "My story"
   And I have upvoted on "My best story"
   When I go to the stories page
   Then I should see a link "My story" to "http://www.writer1.com/story1"
   And I should see "7 votes"
   And I should see a link "My best story" to "http://www.writer2.com/story1"
   And I should see "12 votes"
   And I should not see an upvoting link
   And I should not see a link "Sign in"

For this test we need to define a FactoryGirl factory for Story, so the Background passes:

spec/factories/story.rb
------------------------------------------
Factory.define :story do |story|
  story.title {"My story"}
  story.url {"http://www.mysite.com/story1"}
  story.association :user
end

When we run this feature, we also discover, that there is no path definition for “I should see a link… to…”. Add the following definition to features/step_definitions/web_steps.rb:

Then /^I should see a link "([^\"]*)" to "([^\"]*)"$/ do |text, target|
   if page.respond_to? :should
     page.should have_selector("a", :text => text, :href => target)
   else
     assert page.has_selector?("a", :text => text, :href => target)
   end
end

There are also no path definitions for “I should see an upvoting link for…”, “I should not see an upvoting link”, “I have upvoted on…”. Add the following definitions to features/step_definitions/story_steps.rb:


When /^(?:|I )have upvoted on "([^"]*)"$/ do |title|
  story = Story.find_by_title(title)
  When %{I go to the stories page}
  And %{I follow "^" within "#story_#{story.id} .upvote"}
end

When /^(?:|I )have upvoted on "([^"]*)" comment for "([^"]*)"$/ do |body, title|
  story = Story.find_by_title(title)
  comment = Comment.find_by_body(body)
  When %{I go to to the story page for "#{title}"}
  And %{I follow "^" within "#comment_#{comment.id} .upvote"}
end

Then /^(?:|I )should see an upvoting link for "([^"]*)"$/ do |title|
  story = Story.find_by_title(title)
  if story.nil?
    comment = Comment.find_by_body(title)
    Then %{I should see a link "^" within "#comment_#{comment.id} .upvote"}
  else
    Then %{I should see a link "^" within "#story_#{story.id} .upvote"}
  end
end

Then /^(?:|I )should not see an upvoting link$/ do
  Then %{I should not see a link "^"}
end

This will also take care of the future steps involving comments.

I will leave it to the reader to run and pass the scenarios, and create (or copy over) the index method in the StoriesController, and the corresponding view. The third scenario is an inplace (AJAX) upvoting a story. It’s indicated by the @javascript tag before the scenario. Make sure that your computer has JRuby installed, and that celerity is installed into JRuby, otherwise this scenario will not run.

4. Now, let’s create a feature to view a story and upvote comments in features/visitor_views_story.feature:

Feature: story pages
   In order to see if I'm interested in the story
   As a visitor
   I can read a story page

Background:
   Given the following story exists:
       | title    |  url                          | user                      |
       | My story |  http://www.mysite.com/story1 | email: writer@example.com |
   And the following comment exists:
       | body           |  story           | votes | user                        |
       | This is cool   |  title: My story | 5     | email:commenter@example.com |   
       
Scenario: visitor views a story page
   When I go to the story page for "My story"
   Then I should see a link "My story" to "http://www.mysite.com/story1"
   And I should see "6 votes"
   And I should see a comment "This is cool"
   And I should not see a comment form
   And I should not see an upvoting link
   And I should not see a link "reply"
   And I should see a link "Sign in"


Scenario: registered user views a story page with a comment they haven't upvoted
   Given I have signed in with "me@example.com/test"
   When I go to the story page for "My story"
   Then I should see a link "My story" to "http://www.mysite.com/story1"
   And I should see a comment form
   And I should see an upvoting link for "This is cool"
   And I should see "6 votes"
   And I should see a comment "This is cool"
   And I should see a link "reply"
   And I should not see a link "Sign in"

@javascript
Scenario: registered user views a story page with a comment they have upvoted
   Given I have signed in with "me@example.com/test"
   And I have upvoted on "This is cool" comment for "My story"
   When I go to the story page for "My story"
   Then I should see a link "My story" to "http://www.mysite.com/story1"
   And I should see a comment form
   And I should see "7 votes"
   And I should see a comment "This is cool"
   And I should see a link "reply"
   And I should not see an upvoting link
   And I should not see a link "Sign in"

This is similar to the previous feature, except we need to create the CommentController and the comment model (don’t forget about the comment factory), and also the method show and the corresponding view in the StoriesController. I leave it for the reader to finish.

Advertisements

About RailsBlogger

I'm a Software Developer with over 10 years of experience, Java and Ruby on Rails.

Discussion

No comments yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: