homeASCIIcasts

186: Pickle With Cucumber 

(view original Railscast)

In this episode we’re going to look into the Behaviour Driven Development library Cucumber in a little more depth. We’ve covered Cucumber twice before, in episodes 155 [watch, read] and 159 [watch, read]. This episode will introduce a gem called Pickle which makes working with Cucumber more convenient.

To demonstrate Pickle we’ll start to write a new Rails e-commerce application called store. We’ll start in the usual way.

rails store

To use Pickle and Cucumber we’ll need to add some gems to our application. These gems are only concerned with the application’s testing environment so we’ll add references to them in /config/environments/test.rb.

config.gem "rspec", :lib => false, :version => ">=1.2.9"
config.gem "rspec-rails", :lib => false, :version => ">=1.2.9"
config.gem "webrat", :lib => false, :version => ">=0.5.3"
config.gem "cucumber", :lib => false, :version => ">= 0.4.3"
config.gem "pickle", :lib => false, :version => ">= 0.1.21"

The list above contains the gems that we’ve used in the previous Cucumber episodes and also a reference to Pickle. To make sure that they are all installed we can run

sudo RAILS_ENV=test rake gems:install

to install the correct versions we need.

The next step is to set up Cucumber, which we do by running

script/generate cucumber

This will create the files that Cucumber needs and show a flashing alert telling us that, as we didn’t specify whether we were using RSpec or TestUnit, Cucumber has looked through our gems and chosen rspec for us. We are using RSpec here so that’s fine.

As we’re using Pickle along with Cucumber we’ll have to run its generate script too:

script/generate pickle

This will generate a couple of extra files, including a pickle_steps.rb file which gives us some convenient ways to generate ActiveRecord models in our Cucumber scenarios. We’ll be making use of this soon.

The Product Page

Our application is now set up so we can begin to generate its functionality. We know that we want a product page that will show detailed information about a given product so we’ll need a Product model along with a Products controller that has a show action.

We’ll create a Product model first with name and price attributes. We’re using RSpec so we’ll generate an rspec_model.

script/generate rspec_model product name:string price:decimal

Then we can migrate our database.

rake db:migrate

And clone it for the test environment.

rake db:test:clone

Next we’ll create the products controller and give it a show action. Again, as we’re using RSpec we’re going to create an rspec_controller.

script/generate rspec_controller products show

We can now start defining the behaviour for the show action in a Cucumber feature. In our application’s features directory we’ll create a file called display_products.feature. At the top of the file we’ll define our feature.

Feature: Display Products
	In order to purchase the right product
	As a customer
	I want to browse products and see detailed information

Below this we can start to define the scenario. We’re going to have to create a new product and then check that the show page displays the information for that product properly.

This is where Pickle comes in handy as it is great for generating ActiveRecord models in Cucumber scenarios. If you take a look at the API section of Pickle’s Github page you can see that it provides Cucumber step definitions for generating models. Another nice feature of Pickle is that if you’re using Machinist or Factory Girl to create factory objects it will make use of those to create the models rather than generating them from scratch. The steps that Pickle uses are defined in the pickle_steps.rb file that was generated earlier and it’s worth taking a look at this file to see what steps are provided, especially if you want to customise any of them. We’re not doing anything quite that advanced here so we can get on and define our scenario.

Scenario: Show product
  Given a product exists with name: "Milk", price: "2.99"
  When I go to the show page for that product
  Then I should see "Milk" within "h1"
  And I should see "£2.99"

The first line of this scenario creates a new Product with the name “Milk” and priced at 2.99. Once the product has been created we go to the show page for that product. Normally this would be a difficult step to write but Pickle provides a convenient way to refer to a model that has just been created that allows us to refer to it as that product. This scenario ends with a check to see that that product’s title exists within an h1 element and that the price is displayed with its currency symbol.

Having written our scenario we can run the Cucumber features to see what fails.

$ cucumber features -q
Feature: Display Products
	In order to purchase the right product
	As a customer
	I want to browse products and see detailed information
  Scenario: Show product
    Given a product exists with name: "Milk", price: "2.99"
    When I go to the show page for that product
    Can't find mapping from "the show page for that product" to a path.
      Now, go and add a mapping in /Users/eifion/rails/apps_for_asciicasts/ep186/store/features/support/paths.rb (RuntimeError)
      ./features/support/paths.rb:22:in `path_to'
      ./features/step_definitions/webrat_steps.rb:16:in `/^I go to (.+)$/'
      features/display_products.feature:8:in `When I go to the show page for that product'
    Then I should see "Milk" within "h1"
    And I should see "£2.99"
Failing Scenarios:
cucumber features/display_products.feature:6 # Scenario: Show product
1 scenario (1 failed)
4 steps (1 failed, 2 skipped, 1 passed)
0m0.049s

We have one failing step: Cucumber can’t find a mapping for the show page for that product and doesn’t know how to convert that to a URL path.

This can be fixed by editing the /features/support/paths.rb file to map descriptions to a URL. We could just write a mapping for the products show page, but it will be better if we make the path more generic so that it works with any model. We can make use of one of Pickle’s features to help with this.

when /the show page for (.+)/
  polymorphic_path(model($1))

Pickle provides a method called model to which you can pass a string such as “that product” and which will return the last model of that type that Pickle created. We can pass the argument from the regular expression to model and have it return (in this case) the last Product created.

We want to return the path to that product’s show page and we can do this by making use of a Rails helper method called polymorphic_path method which will do exactly that.

If we run Cucumber again we’ll see a different error message, this time about the undefined method ' product_path'. This is because we’ve not set up the routing for the Product resource yet. We can do that by changing our /config/routes.rb file to look like this:

ActionController::Routing::Routes.draw do |map|
  map.resources :products
end

If we try running our Cucumber features now we’ll have two passing steps.

$ cucumber features -q
Feature: Display Products
	In order to purchase the right product
	As a customer
	I want to browse products and see detailed information
  Scenario: Show product
    Given a product exists with name: "Milk", price: "2.99"
    When I go to the show page for that product
    Then I should see "Milk" within "h1"
      expected the following element's content to include "Milk":
      Products#show (Spec::Expectations::ExpectationNotMetError)
      ./features/step_definitions/webrat_steps.rb:129
      (eval):2:in `within'
      ./features/step_definitions/webrat_steps.rb:128:in `/^I should see "([^\"]*)" within "([^\"]*)"$/'
      features/display_products.feature:9:in `Then I should see "Milk" within "h1"'
    And I should see "£2.99"
Failing Scenarios:
cucumber features/display_products.feature:6 # Scenario: Show product
1 scenario (1 failed)
4 steps (1 failed, 1 skipped, 2 passed)
0m0.034s

The scenario is finding the page, but failing to find the product’s title and price on it which is as we’d expect as the show action is currently empty. We’ll write just enough code to make the scenario pass. In the controller we’ll get the product by its id.

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
  end
end

And in the view (/app/views/products/show.html.erb) we’ll output the product’s title in an h1 element and its price, converted to a currency.

<h1><%= h(@product.name) %></h1>
<p><%= number_to_currency(@product.price, :unit => "£") %></p>

If we run Cucumber one more time our scenario passes.

$ cucumber features -q
Feature: Display Products
	In order to purchase the right product
	As a customer
	I want to browse products and see detailed information
  Scenario: Show product
    Given a product exists with name: "Milk", price: "2.99"
    When I go to the show page for that product
    Then I should see "Milk" within "h1"
    And I should see "£2.99"
1 scenario (1 passed)
4 steps (4 passed)
0m0.028s

That’s it! We’ve successfully implemented the show action and used Pickle to simplify the writing of the scenario.

Generating Multiple Models

Pickle also provides a convenient way to generate multiple models at once. As well as a show action we want our application to have an index page that shows a list of products so we’ll define a scenario in display_products.feature to cover this.

@index
Scenario: List products
  Given the following products exist
    | name   | price |
    | Milk   | 2.99  |
    | Puzzle | 8.99  |
  When I go to the list of products
  Then I should see "Milk"
  And I should see "Puzzle"

This scenario makes use of another of Pickle’s steps. To create a number of products we just need to write Given the following <model>s exist and then define a Cucumber table below it to list the products. Once the products are created the scenario goes to the products index action and checks that each product’s title is shown.

If we run this scenario now it will fail. (Note that we’ve made use of Cucumber’s tagging feature to run just this one scenario.)

$ cucumber features -q -t @index
Feature: Display Products
	In order to purchase the right product
	As a customer
	I want to browse products and see detailed information
  @index
  Scenario: List products
    Given the following products exist
      | name   | price |
      | Milk   | 2.99  |
      | Puzzle | 8.99  |
    When I go to the list of products
      Can't find mapping from "the list of products" to a path.
      Now, go and add a mapping in /Users/eifion/rails/apps_for_asciicasts/ep186/store/features/support/paths.rb (RuntimeError)
      ./features/support/paths.rb:25:in `path_to'
      ./features/step_definitions/webrat_steps.rb:16:in `/^I go to (.+)$/'
      features/display_products.feature:18:in `When I go to the list of products'
    Then I should see "Milk"
    And I should see "Puzzle"
Failing Scenarios:
cucumber features/display_products.feature:13 # Scenario: List products
1 scenario (1 failed)
4 steps (1 failed, 2 skipped, 1 passed)
0m0.019s

The scenario first fails because of a missing path. The way to fix this is to go and add another mapping in paths.rb to map this specific path, but as our application grows and the number of scenarios increases this can quickly become tiresome. One way around this is to define a more generic path and to use URLs in the Cucumber steps. The path we’ll add is this

when /path "(.+)"/
  $1

which will allow us to replace

When I go to the list of products

in the scenario with

When I go to path "/products"

When we run the scenario again it will fail because we don’t have an index action in the ProductsController. This is something that can be fixed fairly easily by first writing the index action.

def index
  @products = Product.all
end

And then creating a view file at /app/views/products/index.html.erb to go with it in which we’ll put just enough code to satisfy the scenario.

<% for product in @products %>
  <p><%= h product.name %>
<% end %>

When we run our features again they all pass.

$ cucumber features -q -t @index
Feature: Display Products
	In order to purchase the right product
	As a customer
	I want to browse products and see detailed information
  @index
  Scenario: List products
   Given the following products exist
     | name   | price |
     | Milk   | 2.99  |
     | Puzzle | 8.99  |
    When I go to path "/products"
    Then I should see "Milk"
    And I should see "Puzzle"
1 scenario (1 passed)
4 steps (4 passed)
0m0.034s

Table Diffs

We’ll finish this episode by showing one more feature: table diffs. This isn’t a Pickle-specific feature but something that has recently been added to Cucumber. It’s an easy way to compare the data in an HTML table with the data in a Cucumber table.

Let’s modify the last scenario we wrote to make use of this by replacing the Then steps.

@index
Scenario: List products
  Given the following products exist
    | name   | price |
    | Milk   | 2.99  |
    | Puzzle | 8.99  |
  When I go to path "/products"
  Then I should see products table
    | Milk   | 2.99  |
    | Puzzle | 8.99  |

This new step is something we’re have to going to have to generate. In the /features/step_definitions directory we’ll create a new file called product_steps.rb in which we’ll put the following step definition:

Then(/^I should see products table$/) do |expected_table|
  expected_table.diff!(table_at("#products").to_a)
end

In this step definition expected_table is the Cucumber table at the end of the scenario above. We can call diff! on this table and compare it to an HTML table with an id of products. If we want the scenario to pass we’ll have to modify the index view so that the products are rendered as a table.

<table id="products">
<% for product in @products %>
  <tr>
    <td><%= h product.name %></td>
    <td><%= number_to_currency product.price, :unit => "£"%></td>
  </tr>
<% end %>
</table>

The step will look at the products table, convert it to an array and compare it to the Cucumber table in the scenario. Let’s run our features again and see what happens.

$ cucumber features -q -t @index
Feature: Display Products
	In order to purchase the right product
	As a customer
	I want to browse products and see detailed information
  @index
  Scenario: List products
    Given the following products exist
      | name   | price |
      | Milk   | 2.99  |
      | Puzzle | 8.99  |
    When I go to path "/products"
    Then I should see products table
      | Milk   | 2.99 | £2.99 |
      | Puzzle | 8.99 | £8.99 |
      Tables were not identical (Cucumber::Ast::Table::Different)
      ./features/step_definitions/product_steps.rb:2:in `/^I should see products table$/'
      features/display_products.feature:19:in `Then I should see products table'
Failing Scenarios:
cucumber features/display_products.feature:13 # Scenario: List products
1 scenario (1 failed)
3 steps (1 failed, 2 passed)
0m0.034s

The scenario fails because the prices in the HTML table have a currency symbol before them which is missing from the Cucumber table that it’s being compared against. If we add the escaped currency symbol to the Cucumber table…

Then I should see products table
  | Milk   | £2.99  |
  | Puzzle | £8.99  |

... then the scenario will pass.

We have to be careful if we have any HTML in the table. For example if the product name was a link to that’s product’s page then the scenario would fail. We can fix this by modifying the step definition so that any HTML tags are removed.

Then(/^I should see products table$/) do |expected_table|
  html_table = table_at("#products").to_a
  html_table.map! { |r| r.map! { |c| c.gsub(/<.+?>/, '') } }
  expected_table.diff!(html_table)
end

The step now loops through each row in the table and strips the HTML tags from it before making the comparison. Now the scenario will pass.

The nice thing about this table diff technique is that we’re comparing everything as it looks on the page. This means that we could change the order that the products appear in or not display some products in the list if, say, we had an “available” attribute and make sure that the user is seeing exactly what they should be.

That’s it for this episode. We’ve covered some of the more advanced features of Cucumber here. If you’re new to Cucumber or need to recap don’t forget to take a look at the first two episodes.

Actually, that’s not quite it. We’ll leave you with a final tip. If you’re ever trying to debug a problem in Cucumber and can’t work out what it is you can add the line

Then show me the page 

to your scenario. As long as you have the launchy gem installed it will open a browser and show you exactly what Webrat is seeing.

Seeing what Webrat sees in a browser.