Book Image

Instant Cucumber BDD How-to

By : Wayne Ye
Book Image

Instant Cucumber BDD How-to

By: Wayne Ye

Overview of this book

<p>Cucumber is a Behavior Driven Design framework, which allows a developer to write specification tests which then tests that the program works as it should. It is a different development paradigm, as it involves writing what the program should do first, then you develop until it passes the tests.<br /><br />Instant Cucumber BDD How-to will cover basics of Cucumber in a Behaviour Driven Development (BDD) style and explain the essence of Cucumber, describe how to write Cucumber features to drive development in a real project, and also describe many pro tips for writing good Cucumber features and steps. Cucumber is a very fun and cool tool for writing automated acceptance tests to support software development in a Behaviour Driven Development (BDD) style.<br /><br />Instant Cucumber BDD How-to will highlight Cucumber's central role in a development approach called Behaviour Driven Development (BDD), describe how to write Cucumber features to drive development in a real project, and finally introduce some famous third-party libraries used inline with Cucumber.</p> <p>It will show you how to carry out all the tasks associated with BDD using Cucumber and write basic Cucumber steps. It will assist you in using Pro tips for writing expressive Gherkin and implement guidelines for writing DRY steps. You'll learn how to use Cucumber's Gherkin to describe the behavior customers want from the system in a plain language.</p>
Table of Contents (7 chapters)

Building a real web application with Cucumber (Intermediate)


In this recipe we are going to build a real web application with a BDD style and process. You will learn how to work with behavior-driven software development using Cucumber.

Getting ready

Assume we are in a web development team and are cooperating with a product manager. Our goal is to develop a simple web blog system, and we have already had a meeting and summarized several user stories as follows:

  • Story #1: As a blog owner, I can write new blog posts.

  • Story #2: As a blog visitor, I can see a list of posted blogs.

  • Story #3: As a blog owner, I can edit my blog posts.

  • Story #4: As a blog visitor, I can input comments onto the blog.

  • Story #5: As a blog owner, I can delete comments.

How to do it...

  1. Out first step is to let Rails generate a new application called blog:

    $ rails new blog --skip-test-unit
    

    And we need the following Ruby gems in Gemfile:

    group :test do
    gem 'rspec-rails'
    gem 'cucumber-rails'
    gem 'capybara'
    gem 'launchy'
    gem 'database_cleaner'
    end
  2. After bundle install, we install Cucumber into the blog project:

    $ rails generate cucumber:install
    

Story #1: As a blog owner, I can write new blog posts

  1. We wait until Rails has finished generating Cucumber files. Then we can start writing the first Cucumber scenario for this story. We add a feature file under the features directory named write_post.feature:

    Feature: Write blog
    As a blog owner 
    I can write new blog post
     Scenario: Write blog
       Given I am on the blog homepage
       When I click "New Post" link
       And I fill "My first blog" as Title
       And I fill "Test content" as content
       And I click "Post" button
       Then I should see the blog I just posted
  2. Let's run the write_post.feature and watch it fail:

    cucumber features/write_post.feature:

    Yes it fails, which is good and as expected; now we have work to do, that is to implement this feature (also a real story)!

  3. So we go to our favorite terminal and have Rails help us generate a Post scaffold:

    $ rails generate scaffold Post title content:textpost_time:datetime
    
  4. We perform a database migration for both development and test environments:

    $ rakedb:migrate
    $ RAILS_ENV=test rake db:migrate
    
  5. We now start implementing the Cucumber step for write_post. A little noticeable point is using @title to record the entered title for future expected use. The code is shown as follows:

    Given /^I am on the blog homepage$/ do
     visit("/posts")
    end
    
    When /^I click "New Post" link$/ do
     click_on "New Post"
    end
    
    When /^I fill "(.*?)" as Title$/ do |title|
     @title = title
     fill_in "Title", :with => title
    end
    
    When /^I fill "(.*?)" as content$/ do |content|
     fill_in "Content", :with => content
    end
    
    When /^I click "(.*?)" button$/ do |btn|
     click_button btn
    end
    
    Then /^I should see the blog I just posted$/ do
     page.should have_content(@title)
    end
  6. Ok, now we rerun the test and it should pass:

  7. And if we open the browser to do a manual test on http://localhost:3000/posts, we can see it works as expected. The following is a screenshot of the blog home page:

  8. The screenshot of the write blog page is as follows:

Story #2: As a blog visitor, I can see a list of posted blogs

  1. We create a new feature named show_blog_list.feature, and we assume there already exists four blog posts:

    Feature: Show blog list
    As a blog visitor 
    I can see list of posted blogs
    
     Scenario: Show blog list
       Given there are already 4 posts
    And I am on the blog homepage
       Then I can see list of 4 posted blogs
  2. The Given step is exactly the same with the "write blog" feature. We definitely shouldn't repeat ourselves. The "posts preparation" step seems very common, so we can create a common_steps.rb under the step_definitions directory. After that we move the step Given I am on the blog homepage from write_blog_steps to common_steps and create a shared step for preparing blog posts:

    Given /^I am on the blog homepage$/ do
      visit("/posts")
    end
    
    And /^there are already (\d) posts$/ do |count|
      count.to_i.times do |n|
        Post.create!({ :title => "Title #{n}", :content => "Content #{n}", :post_time => Time.now })
      end
    end
  3. Ok, now we run show_blog_list.feature and watch that it fails. We will see that the two Given steps have already been implemented within common_steps:

  4. Following the guide we create show_blog_list_steps.rb. In the step we expect there to be a list of blogs wrapped within an HTML table with an ID of posts-list, and since we prepared four blog posts we expect there to be five rows in the table, so we firstly write our testing code as follows:

    Then /^I can see list of (\d) posted blogs$/ do |count|page.should have_selector("table#posts-list>tr:eq(#{count})")end
  5. Now it is time to write real code which is driven by the preceding described behavior code. We modify app/views/posts/index.html.erb as follows:

    <h1>Listing posts</h1>
    
      <table id="posts-list">
        <tr>
          <th>Title</th>
          <th>Content</th>
          <th>Post time</th>
          <th></th>
          <th></th>
          <th></th>
        </tr>
    
        <% @posts.each do |post| %>
        <tr>
          <td><%= post.title %></td>
          <td><%= post.content %></td>
          <td><%= post.post_time %></td>
          <td><%= link_to 'Show', post %></td>
          <td><%= link_to 'Edit', edit_post_path(post) %></td>
          <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
        </tr>
        <% end %>
      </table>
    
      <br />
    
      <%= link_to 'New Post', new_post_path %>
  6. Now we rerun show_blog_list.feature. It passed, yeah!

Story #3: As a blog owner, I can edit my blog posts

  1. We add a new feature named edit_blog.feature:

    Feature: Edit blog
    As a blog owner
    I can edit my blog posts
    
     Scenario: Edit blog 
       Given there is a post with title "Dummy post" and content "Dummy content"
       And I am on the blog homepage
       When I edit this post
       And I update title to "Updated title" and content to "Updated content"
       Then I can see it has been updated
  2. As usual, we first run it and watch it fail, and then create edit_blog_steps.rb with test code as follows:

    Given /^there is a post with title "(.*?)" and content "(.*?)"$/ do |title, content|
      @post = Post.create!({ :title => title, :content => content, :post_time => Time.now })
    end
    
    When /^I edit this post$/ do
     visit(edit_post_url @post)
    end
    
    When /^I update title to "(.*?)" and content to "(.*?)"$/ do |title, content|
      @updated_title = title
      @updated_content = content
      @post.update_attributes!({ :title => @updated_title, :content => @updated_content })
    end
    
    Then /^I can see it has been updated$/ do
      step %{I am on the blog homepage}
      find("table#posts-list>tr:eq(2) >td:eq(1)").should have_content(@updated_title)
      find("table#posts-list>tr:eq(2) >td:eq(2)").should have_content(@updated_content)
    end
  3. When we go back to the terminal and rerun the edit_blog.feature, it should now pass. The following screenshot shows edit_blog.feature has run successfully:

Hooray! We've finished three stories so far. They are all around posts creating, editing, and viewing. There are two more stories related with comments. Let's starting developing them with the behavior-driven development style!

Story #4: As a blog visitor, I can input comments onto the blog

  1. We first create an input_comment.feature for this story:

    Feature: Input comment
    As a blog visitor
    I can input comment onto blog
    
     Scenario: Input comment
       Given there is a post titled with "Dummy post" and content with "Dummy content"
       And I am on the post page
       When I add a comment with the following information
         | Name    | Email             | Content         |
         | Wayne   | [email protected] | Test comment    | 
       Then I can see the comment has been added onto the post
  2. We run the feature and watch it fail:

  3. We create input_comment_steps.rb and write the test code as follows:

    Given /^I am on the post page$/ do
     visit(post_path @post)
    end
    
    When /^I add a comment with the following information$/ do |table|
     # table is a Cucumber::Ast::Table
     table.hashes.each do |comment_data|
       @commenter = comment_data[:name]
       @email = comment_data[:email]
       @content = comment_data[:content]
       @post.comments.create!({ :name => @commenter, :email => @email, :content => @content })
     end
    end
    
    Then /^I can see the comment has been added onto the post$/ do
     comments_list = find("div#comments-list")
     comments_list.should have_content(@commenter)
     comments_list.should have_content(@email)
     comments_list.should have_content(@content)
    end
  4. To make our test pass, we let Rails help us to generate a Comment scaffold:

    $ rails generate scaffold Comment post:references name email content
    
  5. Database migration is as follows:

    $ rakedb:migrate && RAILS_ENV=test bundle exec rake db:migrate
    
  6. After the migration is done, we update several places in the Rails-generated code. First is routes.rb. We specify comments as nested resources under blogs:

    resources :blogs do
     resources :comments
    end
  7. We've specified that Comment belongs to Post; we need to update Post to contain many comments as well, in post.rb:

    has_many :comments

    In CommentsController, we update the create action to load the post object that the created comment belongs to:

    def create
     @comment = Comment.new(params[:comment])
     @comment.post = Post.find_by_id(params[:post_id])
    
     respond_to do |format|
       if @comment.save
         format.html { redirect_to @comment.post, notice: 'Comment was successfully created.' }else
         format.html { render action: "new" }
         format.json { render json: @comment.errors, status: :unprocessable_entity }
       end
     end
    end
  8. And finally we update the view template app/views/posts/show.html.erb for showing a post:

    <h2>&darr;Comments&darr;</h2>
    <div id="comments-list">
      <% @post.comments.each_with_index do |c,idx| %>
      <p><span>#<%= idx + 1 %>: <%= c.name %></span>: <%= c.content %></p>
      <% end %>
    
      <hr />
    
      <%= form_for([@post, @post.comments.build]) do |f| %>
      <div class="field">
        <%= f.label :name %><br />
        <%= f.text_field :name %>
      </div>
      <div class="field">
        <%= f.label :email %><br />
        <%= f.text_field :email %>
      </div>
      <div class="field">
        <%= f.label :content %><br />
        <%= f.text_area :content %>
      </div>
      <div class="actions">
        <%= f.submit %>
      </div>
      <% end %>
    </div>
  9. At this time, we should be able to pass the input comment feature. The following screenshot shows that input_comment.feature has run successfully:

Story #5: As a blog owner, I can delete comments

  1. As usual we create a delete_comment.feature:

    Feature: Delete comment
    As a blog owner
    I can delete comment
    
     Scenario: Delete comment
       Given there is a post titled with "Dummy post" and content with "Dummy content"
       And there is a comment on this post
       When I am on the post page
       And I click "Delete Comment" 
       Then the comment should be deleted
  2. Run it and watch it fail, and then implement the steps inside delete_comment_steps.rb as follows:

    Given /^there is a comment on this post$/ do
      @post.comments.create!({ :name => "Wayne", :email => "[email protected]", :content => "Test deleting comment" })
    end
    
    When /^I click "Delete Comment"$/ do
      click_on "Delete Comment"
    end
    
    Then /^the comment should be deleted$/ do
      find("#comments-list").should have_no_content("Wayne") 
    end
  3. To make the test pass, we need to update show.html.erb to add the Delete Comment link to each comment:

    <% @post.comments.each_with_index do |c,idx| %>
     <p>
       <span>#<%= idx + 1 %>: <%= c.name %></span>: <%= c.content %> 
       <%= link_to "Delete Comment", post_comment_path(@post, c), :method => :delete, :confirm => "Are you sure you want to delete this comment?" %>
     </p>
    <% end %>
  4. Now we rerun delete_comment.feature. It passed successfully:

How it works...

In this recipe we developed a very simple blog application with BDD using Cucumber. We split the requirement into five user stories, and then transformed them into Cucumber features. Next we implemented each story one by one, strictly following the BDD process.

Most of the code was generated by Rails, so we actually wrote very few lines of product code, because our goal is to learn how to use Cucumber to drive a real user story development, so that we are getting used to BDD with one story by another, and one iteration by another, eventually delivering the software.

The essence of BDD using Cucumber is that it describes a feature and its expected behavior. So that drives the development under the definiteness (ideally no misunderstanding); on the other hand, each Cucumber feature is just like an acceptance test case, which can be easily integrated with continuous integration.