159: More on Cucumber
(view original Railscast)
Episode 155 was all about Cucumber, a high-level testing framework. This episode will show some of its more advanced features, so if you haven’t seen the other episode then it’s well worth catching up before reading this. We’ll also be using Webrat and Factory Girl which were covered in episodes 156 and 158.
Updating Cucumber
Cucumber has recently been updated to version 0.3.0, so the first thing we’ll need to do is update the version of the gem that our application’s using. In /config/environments/test.rb
we’ll change the reference to Cucumber so that it uses the latest version.
config.gem "rspec", :lib => false, :version => ">=1.2.2" config.gem "rspec-rails", :lib => false, :version => ">=1.2.2" config.gem "webrat", :lib => false, :version => ">=0.4.3" config.gem "cucumber", :lib => false, :version => ">=0.3.0" config.gem "thoughtbot-factory_girl", :lib => "factory_girl", :source => "http://gems.github.com"
Next we’ll run rake to make sure the gems are all up to date.
sudo rake gems:install RAILS_ENV=test
Finally we’ll regenerate Cucumber to update the files that have changed. As we’ve used Cucumber in this application before the new version will want to overwrite some of the files. We can let it overwrite webrat_steps.rb
and env.rb
, but as we’ve already made changes to paths.rb
, we’ll have to update it manually.
$ script/generate cucumber exists features/step_definitions overwrite features/step_definitions/webrat_steps.rb? (enter "h" for help) [Ynaqdh] y force features/step_definitions/webrat_steps.rb exists features/support overwrite features/support/env.rb? (enter "h" for help) [Ynaqdh] y force features/support/env.rb overwrite features/support/paths.rb? (enter "h" for help) [Ynaqdh] n skip features/support/paths.rb exists lib/tasks identical lib/tasks/cucumber.rake identical script/cucumber
The paths.rb
file contains a list of the mappings from the names we give the pages in our Cucumber definitions to the actual paths in the application. The part that needs to change to make this file work with Cucumber 0.3 is the section at the end that adds this module to Cucumber’s World
. (The World
is the context in which Cucumber runs its scenarios.)
module NavigationHelpers def path_to(page_name) case page_name when /the homepage/ root_path when /the list of articles/ articles_path else raise "Can't find mapping from \"#{page_name}\" to a path." end end end World do |world| world.extend NavigationHelpers world end
Cucumber 0.3 has a simpler syntax for this and so we can replace the bottom four lines in the code above with this one.
World(NavigationHelpers)
A Quick Check
As a reminder of the steps we already have and to make sure we’re starting with a passing set, we’ll run the current set of scenarios.
$ cucumber features -n Feature: Manage Articles In order to make a blog As an author I want to create and manage articles Scenario: Articles List Given I have articles titled Pizza, Breadsticks When I go to the list of article Then I should see "Pizza" And I should see "Breadsticks" Scenario: Create Valid Article Given I have no articles And I am on the list of articles When I follow "New Article" And I fill in "Title" with "Spuds" And I fill in "Content" with "Delicious potato wedges!" And I press "Create" Then I should see "New article created." And I should see "Spuds" And I should see "Delicious potato wedges!" And I should have 1 article 2 scenarios 14 passed steps
Our First New Scenario
Now that we know that our current scenarios are all passing we can begin to add some new functionality. Below is the profile page for a user; we want to add a link to it so that if a user is viewing their own profile they have the option to edit it. The link should also be visible to admins who should be able to edit anyone’s profile.
We’ll be using Behaviour-Driven Development to implement this feature so we’ll start off by defining the behaviour as a Cucumber feature. In the features folder we’ll create a new file called manage_users.feature in which we’ll put the feature and its scenarios. At the top of the file we’ll define the feature.
Feature: Manage Users In order manage user details As a security enthusiast I want to edit user profiles only when authorized
The first scenario we’ll write is the one that says that the edit link should be visible for an admin. For this scenario we’ll define two users, one “normal” user and an admin. To help us describe the users we’ll make use of Cucumber’s tables which provide a neat way of defining data in a scenario.
Scenario: Show edit link as admin Given the following user records | username| password| admin | | bob | secret | false | | admin | secret | true | And I am logged in as "admin" with password "secret" When I visit profile for "bob" Then I should see "Edit Profile"
The first row in the table defines the column names, and each subsequent row represents a a record. The columns are separated by a vertical bar. The vertical bars don’t need to be aligned but it makes the data easier to read if you do. If you have the Cucumber bundle for Textmate5 installed then you can select the table and use the shortcut CMD+OPT+\ to automatically tidy the table data up.
The rest of the scenario says that when we’re logged in as admin an we visit Bob’s profile page we should see the “Edit Profile” link.
If we run Cucumber now we’ll see the steps we need to define. (The output from the other scenarios isn’t reproduced below.)
Feature: Manage Users In order manage user details As a security enthusiast I want to edit user profiles only when authorized Scenario: Show edit link as admin Given the following user records | username | password | admin | | bob | secret | false | | admin | secret | true | And I am logged in as "admin" with password "secret" When I visit profile for "bob" Then I should see "Edit Profile" 3 scenarios 1 skipped step 3 undefined steps 14 passed steps You can implement step definitions for undefined steps with these snippets: Given /^the following user records$/ do |table| # table is a Cucumber::Ast::Table pending end Given /^I am logged in as "([^\"]*)" with password "([^\"]*)"$/ do |arg1, arg2| pending end When /^I visit profile for "([^\"]*)"$/ do |arg1| pending end
Tagging Features And Scenarios
As well as the steps for our current feature Cucumber will show the output from all of the other scenarios. If we had a hundred or so scenarios in our application there would be a lot of output generated which would make it difficult to focus on our current scenario. Cucumber supports tagging of scenarios which means that we can give scenarios tags and run only the scenarios that have a given tag. We do this by adding a word preceded by an @ before the scenario. We’ll tag our new scenario with the tag @focus
so that we can focus on just running that one scenario.
@focus Scenario: Show edit link as admin (rest of scenario omitted)
To run only scenarios with a certain tag we pass Cucumber a -t
option and the name of the tag.
cucumber features -n -t focus
Now only our tagged scenario will be run. To run all of the scenarios except the ones with a certain tag we place a tilde before the tag name.
cucumber features -n -t ~focus
Note that depending on which shell you’re running you might need to escape the tilde with a backslash.
We can also apply tags to features. Tags applied to a feature will also apply to any scenarios that feature has so we’ll move our @focus
tag to our new feature so that we can run just that feature’s scenarios.
@focus Feature: Manage Users In order manage user details (rest of feature omitted)
Defining The Steps For Our Scenario
When we ran Cucumber before it provided some skeleton code for the undefined steps in our scenario. We’ll create a file called user_steps.rb
to put this code in. This file should go in the /features/step_definitions
directory.
Given /^the following user records$/ do |table| # table is a Cucumber::Ast::Table pending end Given /^I am logged in as "([^\"]*)" with password "([^\"]*)"$/ do |arg1, arg2| pending end When /^I visit profile for "([^\"]*)"$/ do |arg1| pending end
The first step has a table object passed to its block which, as the comment states, is a custom Cucumber object. To work with the data in the table we only need to use one of that object’s methods: hashes
. Calling table.hashes
will return an array of hashes. Each element in the array is a hash representing one line of the table. We can use the data in the hash to create our users, but as there’s not enough data in the table to create a valid user instead we’ll use a Factory. We have Factory Girl set up in our application and a factory defined for the user model so we can pass the hash that represents the user in the table to the factory to create each user. The first step now looks like this.
Given /^the following user records$/ do |table| table.hashes.each do |hash| Factory(:user, hash) end end
We have our factories defined in /spec/factories.rb
, but they won’t be available to Cucumber by default. To make them available we’ll add the following line to /features/support/env.rb
so that the factories are added to Cucumber’s environment.
require "#{Rails.root}/spec/factories"
Before we go on to define any more of our steps we’ll take another look at the step we’ve just defined. It performs a useful task and it looks like it could be adapted to work with any model, rather than just with users. We can do this be replacing the word user
with a variable in the regular expression and passing that variable to the block. The variable can then be used to define the type of factory object we want to use.
Given /^the following (.+) records?$/ do |factory, table| table.hashes.each do |hash| Factory(factory, hash) end end
We now have a useful generic step that can be used to turn any Cucumber table into a number of models.
With that done we can now move on and complete the other two steps. The first step covers logging a user in.
Given /^I am logged in as "([^\"]*)" with password "([^\"]*)"$/ do |arg1, arg2|
If we were using a lower-level testing framework we’d create a logged-in user by directly updating the session or something similar, but with Cucumber you’re encouraged to use the routes so we’ll need to go through the login page. We’ll use Webrat to write this step. (If you need to find our more about Webrat you can read or watch episode 156.) We’ll write code to fill in the username and password fields and click the button on the form to log the user in.
Given /^I am logged in as "([^\"]*)" with password "([^\"]*)"$/ do |username, password| visit login_url fill_in "Username", :with => username fill_in "Password", :with => password click_button "Log in" end
For the final step we just need to find the user by their username and visit their profile page.
When /^I visit profile for "([^\"]*)"$/ do |username| user = User.find_by_username!(username) visit user_url(user) end
It might seem to have been a lot of work to create these three steps, but we’ve created at least one fairly generic step that we’ll be able to reuse later.
If we run Cucumber now the step will fail because we haven’t added the “Edit Profile” link to the profile page. We’ll do that now. Because we’re using BDD we’ll only write enough code to make the step pass, so there’s no code to restrict who can see the link. The link should be added to /app/views/users/show.html.erb
.
<% title "Profile for " + @user.username %> <p>This user currently has no bio.</p> <p>Email: <%=h @user.email %></p> <%= link_to "Edit Profile", edit_user_path(@user) %>
Our scenario’s steps now all pass.
$ cucumber features -n -t focus @focus Feature: Manage Users In order manage user details As a security enthusiast I want to edit user profiles only when authorized Scenario: Show edit link as admin Given the following user records | username | password | admin | | bob | secret | false | | admin | secret | true | And I am logged in as "admin" with password "secret" When I visit profile for "bob" Then I should see "Edit Profile" 1 scenario 4 passed steps
We know that our code isn’t correct yet because the “Edit Profile” link is currently visible to everyone. We need to write another scenario that defines the correct behaviour for when the page is visited by someone who isn’t logged in.
Scenario: Hide edit link as guest Given the following user records | username | password| admin | | bob | secret | false | | admin | secret | true | When I visit profile for "bob" Then I should not see "Edit Profile"
This scenario is similar to the last one, but we’ve removed the line that logs the user in and we’re now not expecting to see the “Edit Profile” link. If we run the step it will fail because there are no conditions to restrict who can view the link. As our previous scenario said that admins should see the link to make the test pass we’ll add a condition that restricts the link to admin users.
<% title "Profile for " + @user.username %> <p>This user currently has no bio.</p> <p>Email: <%=h @user.email %></p> <% if current_user && current_user.admin? %> <p><%= link_to "Edit Profile", edit_user_path(@user) %></p> <% end %>
The change we’ve made means that only admins will see the “Edit Profile” link, but we want a user to be able to edit his own profile so we’ll need one more scenario to describe that behaviour. The scenario is very similar to the one we wrote for the admin.
Scenario: Show edit link as owner Given the following user records | username | password | admin | | bob | secret | false | | admin | secret | true | And I am logged in as "bob" with password "secret" When I visit profile for "bob" Then I should see "Edit Profile"
Again the scenario will fail. We just need to make a small adjustment to make it pass.
<% title "Profile for " + @user.username %> <p>This user currently has no bio.</p> <p>Email: <%=h @user.email %></p> <% if current_user && current_user.admin? || current_user == @user %> <p><%= link_to "Edit Profile", edit_user_path(@user) %></p> <% end %>
Now all three of the scenarios pass.
Removing Repetition
Our scenarios now pass but there’s a lot of duplicated code across them and as we write more this will only get worse. We can reduce the duplication by moving the duplicated code that defines the user records into a background clause.
@focus Feature: Manage Users In order manage user details As a security enthusiast I want to edit user profiles only when authorized Background: Given the following user records | username | password | admin | | bob | secret | false | | admin | secret | true | Scenario: Show edit link as admin Given I am logged in as "admin" with password "secret" When I visit profile for "bob" Then I should see "Edit Profile" Scenario: Hide edit link as guest When I visit profile for "bob" Then I should not see "Edit Profile" Scenario: Show edit link as owner Given I am logged in as "bob" with password "secret" When I visit profile for "bob" Then I should see "Edit Profile"
The background will run before each scenario in the feature and populate the user records and allow us to remove some of the duplication in the code. The scenarios still have some duplication in them though. Each one performs similar steps with only slight differences. We can reduce some of this by using Cucumber’s Scenario Outlines.
Scenario Outlines allow us to replace parts of the scenario with variables. We can then use another Cucumber table to provide the data for those variables. Our feature now looks like this:
@focus Feature: Manage Users In order manage user details As a security enthusiast I want to edit user profiles only when authorized Scenario Outline: Show or hide edit profile link Given the following user records | username | password| admin| | bob | secret | false| | admin | secret | true | Given I am logged in as "<login>" with password "secret" When I visit profile for "<profile>" Then I should <action> Examples: | login | profile | action | | admin | bob | see "Edit Profile" | | bob | bob | see "Edit Profile" | | | bob | not see "Edit Profile" | | bob | admin | not see "Edit Profile" |
When the feature runs now it will loop through each row in the Examples table and replace the variable names between the angle brackets with the appropriate column in the examples table. This makes it even easier for us to add a new scenario to the table, as all we have to do is add another row to the table. In the table above we’ve done just that to add another scenario that says that when we log in as bob
we shouldn’t see the link on the admin’s profile page.
There’s one other small change we’ll need to make to use the Examples
table. One of the rows in the table has a blank login value, which we want to mean that the page should be viewed as a guest. We’ll alter user_steps.rb
so that it only runs the Webrat steps to log a user in if a username is supplied.
Given /^I am logged in as "([^\"]*)" with password "([^\"]*)"$/ do |login, password| unless username.blank? visit login_url fill_in "login", :with => login fill_in "Password", :with => password click_button "Log in" end end
We’ll run our features one last time to make sure they still run.
$ cucumber features -n -t focus @focus Feature: Manage Users In order manage user details As a security enthusiast I want to edit user profiles only when authorized Scenario Outline: Show or hide edit profile link Given the following user records | username | password | admin | | bob | secret | false | | admin | secret | true | Given I am logged in as "<login>" with password "secret" When I visit profile for "<profile>" Then I should <action> Examples: | login | profile | action | | admin | bob | see "Edit Profile" | | bob | bob | see "Edit Profile" | | | bob | not see "Edit Profile" | | bob | admin | not see "Edit Profile" | 4 scenarios 16 passed steps
They do, each row in the examples table is parsed and the scenario run with the appropriate values.
That concludes our look at some of the more advanced features in Cucumber. I hope you’ve found it useful.