Posted over 5 years ago. Visible to the public.

How to not repeat yourself in Cucumber scenarios

It is good programming practice to Don't Repeat Yourself (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:

Copy
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:

Copy
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:

Copy
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 are a way to run the same Cucumber scenaro multiple times, but use different placeholder values for each iteration:

Copy
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:

Copy
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.

By refactoring problematic code and creating automated tests, makandra can vastly improve the maintainability of your Rails application.

Owner of this card:

Avatar
Henning Koch
Last edit:
16 days ago
by Jakob Scholz
Attachments:
many_steps.rb
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Henning Koch to makandra dev
This website uses cookies to improve usability and analyze traffic.
Accept or learn more