How to not repeat yourself in Cucumber scenarios

Updated . Posted . Visible to the public.

It is good programming practice to Don't Repeat Yourself Show archive.org snapshot (or DRY). In Ruby on Rails we keep our code DRY by sharing behavior by using inheritance, modules, traits or partials.

When you reuse behavior you want to reuse tests as well. You are probably already reusing examples in unit tests. Unfortunately it is much harder to reuse code when writing integration tests with Cucumber, where you need to express yourself with Gherkin and step definitions instead of Ruby classes and methods.

But don't dispair! Below you will find many different ways to share code between Cucumber scenarios, allowing you to keep your integration tests as DRY as your application code.

Option 1: Call other step definitions

This is Cucumbers default way of sharing short setup steps or assertions. You can even call step definitions from other step definitions by calling steps:

When /^I search for "(.+?)"$/ do |query|
  steps %{
    When I go to the search form
    And I fill in "Query" with "#{query}"
    And I press "Search"
  }
end

Calling other step definitions with steps has two major limitations:

  • The example above calls other step definitions by piecing together strings. This is a cumbersome way of talking to other code, especially if you are calling step definitions with parameters. You would prefer to use vanilla Ruby methods instead. You can do this by Capybara or by using a test harness (see below).

  • When calling steps with multiple lines of Cucumber you lose meaningful stack traces. If a step fails you will always get the same file and line number: The one where you call steps. This is a big deal in practice. To remedy this, check out our many_steps helper (see below).

Option 2: Use our many_steps helper

many_steps is a drop-in replacement for Cucumber's steps helper. It does everything that steps does, but gives you meaningful stack traces in case something goes wrong.

To use this helper, copy the attached file to features/support. Now you can simply call many_steps instead of steps:

When /^I search for "(.+?)"$/ do |query|
  many_steps(<<-GHERKIN)
    When I go to the search form
    And I fill in "Query" with "#{query}"
    And I press "Search"
  GHERKIN
end

If a line is undefined or fails, the stack trace will point to the correct file and line number.

Note that the example above uses Ruby HEREDOC to enter the steps. Actually you can hand any string to many_steps, but using a HEREDOC section named GHERKIN gives you Cucumber syntax highlighting in RubyMine.

Option 3: Use a test harness

A test harness is a Ruby module that you include in the Cucumber world. This way the module's methods become available to all step definitions. Think of it as enhancing your Capybara API with app-specific helper methods from your application domain.

Calling methods from a test harness is usually much more convenient than calling other step definitions. Because you are using plain ruby, you can use return values, structured arguments (e.g. hash options), etc.

I often have files like the session_steps.rb (below) that first define a test harness and then multiple step definitions. The step definitions are a simple wrapper that translate Cucumber regexps to calls of the harness:

module SessionStepsHarness

  def current_user_name
    element = page.first('.current_user')
    element && element.text
  end

  def signed_in?
    current_user_name.present?
  end

  def sign_in(user, password = 'secret')
    visit new_session_path
    fill_in 'E-mail', :with => user.email
    fill_in 'Password', :with => password
    click_button 'Sign in'
  end

  def sign_out
    click_link 'Sign out'
  end

end

World(SessionStepsHarness)
Then /^I should be signed in$/ do
  should be_signed_in
end

Then /^I should be signed in as "([^\"]+)"$/ do |identity|
  current_user_name.should == identity
end

Then /^I should be signed out$/ do
  should_not be_signed_in
end

When /^I sign out$/ do
  sign_out
end

When /^I am signed out$/ do
  sign_out if signed_in?
end

When /^I sign in with "([^\"]+)\/([^\"]+)"$/ do |email, password|
  user = User.where(:email => email).find_or_create
  sign_in(user, password)
end

Option 4: Use scenario outlines

Scenario outlines Show archive.org snapshot are a way to run the same Cucumber scenario multiple times, but use different placeholder values for each iteration:

Scenario Outline: Only some roles have access to the address book
  When I sign in as a <role>
    And I go to the address book
  Then I should be <access> access
  Examples:
    | role   | access  |
    | admin  | granted |
    | sales  | granted |
    | typist | denied  |
    | staff  | denied  |

While scenario outlines can be a good fit, their expressiveness is extremely limited. E.g. you cannot express conditions in an outline, you must make do with placeholder variables in an otherwise static scenario script.

Also when your examples have too many variables, your scenario can devolve into something like this:

Given <records>
  And I <setup>
Then <observation>

Now you have simply pushed all the complexity into the Examples block and you are no longer reusing meaningful amounts of code. It's also a bad way to use Cucumber. Consider using a test harness or the many_steps helper instead.

Henning Koch
Last edit
Rebecca
Attachments
License
Source code in this card is licensed under the MIT License.
Posted by Henning Koch to makandra dev (2013-10-11 16:40)