#186 Pickle with Cucumber
- Download:
- source codeProject Files in Zip (96.1 KB)
- mp4Full Size H.264 Video (26.7 MB)
- m4vSmaller H.264 Video (17.6 MB)
- webmFull Size VP8 Video (45.5 MB)
- ogvFull Size Theora Video (35.9 MB)
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.