← back to all talks and articles

Tips for writing better Cucumber steps

Cucumber is a nice way of documenting user-level acceptance tests in web applications. I’m not a huge fan of it myself, because the collection of step definitions in a typical project tends to grow into an unwieldy mess — but when I do use Cucumber in Rails projects, I tend to use a couple of tricks to keep my steps sane. Here are some of the non-obvious tricks.

1. Use markup conventions to write generic steps

We want to avoid having to write thousands of step definitions. You can take advantage of conventions in naming and markup to define steps that are generic enough to be re-used. Take this step definition for example:

Then(/^I see (\d+) (.*?) rows$/) do |n, class_name|
  n = n.to_i
  class_name = class_name.parameterize.underscore
  expect(page).to have_css("tbody tr.#{class_name}", count: n)
end

You can use this step when you have view templates that display records in tables, to test that a certain number of records is displayed. When you have markup like this:

<table>
  <thead>
    <tr>
      <th>Title</th>
    </tr>
  </thead>
  <tbody>
    <%= content_tag_for(:tr, @posts) do |post| %>
      <td><%= post.title %></td>
    <% end %>
  </tbody>
</table>

…you can use the step definition above like so:

Then I see 3 post rows

Note the class_name.parameterize.underscore part ensures human names such as “paid invoices” or “Registered users” become “paid_invoices” and “registered_users”, which works nicely with the prefix supported by content_tag_for (read more). You could also use this trick to test for content, such as:

Then(/^(.*?) row (\d+) contains "(.*?)"$/) do |class_name, n, text|
  class_name = class_name.paramterize.underscore
  within "table tbody tr.#{class_name}:nth-child(#{n})" do
    expect(page).to have_content(text)
  end
end
# Example usage:
# Then invoice row 3 contains “$ 17.50”

This example focuses on table rows, but is easily adapted into more generic terms, so you can simply state that the page should contain “4 users” or “an invoice”. Writing such steps makes your scenarios quite readable, and forces you into the habit of providing meaningful classes and markup to your templates.

The important take-away here is that you use good front-end development practices (writing meaningful markup) and write highly re-usable steps without too much coupling. This requires a little magic sometimes, but it is worth the effort.

2. Use Transform steps to reduce boilerplate

Transform steps are a nice secret of Cucumber. They allow you to transform step arguments by matching them against a regular expression. The best example I can think of is a count argument, like in the example above. Capturing an argument matching only digits in a regular step definition is easy enough, but we still have to convert its string value to an integer every time. Enter transform steps:

Transform(/^an?$)/) do |str|
  1
end

Transform(/^-?\d+$)/) do |str|
  str.to_i
end

These two transform steps will automatically transform every regular step argument matching these regular expressions (strings like “1”, “-24” or “a”) into integers. Your step definition could look like this:

Given(/^there (?:is|are) (an?|-?\d+) invoices?$/) do |n|
  FactoryGirl.create_list :invoice, n
end

This trick can also be useful for dealing with dates and times, factories, money and other special types of data where you don’t want to deal with plain strings. You can even use them to operate on tables. See the Cucumber wiki on Step Argument Transforms for more information.

A word of caution though: transforms apply everywhere. You will have to make them unique enough to only match where appropriate. For example, you might be tempted to write a Transform step to parse natural language dates and times using the chronic library. But consider the regular expression to match a string argument to be parsed as a date… it would have to match just about anything — most likely leading to conflicts with other Transform steps.

Transform steps are global and the first matching Transform step will be used to transform the argument; there is no cascading or priority. The case of parsing natural-language dates would be better solved in a specific, regular step definition, where your can rely on argument order rather than format to decide how to parse it:

# Given I last signed in two weeks ago
Given(/^I last signed in (.+)$/) do |date|
  date = Chronic.parse(date)
  # ...
end

3. Use meta-programming to define factory steps

If you use FactoryGirl to insert sample date before your tests, you could use meta-programming to generate step definitions. Since FactoryGirl can give you the names of all the factories it knows, you could define a step for each. You will, of course, need a little string-crunching magic to make human-friendly steps:

FactoryGirl.factories.each do |factory|
  # create a human-friendly factory name
  factory_name = factory.name.to_s.humanize.downcase

  # generate a regex fragment matching both plural and singular forms
  factory_matcher = [factory_name, factory_name.pluralize].join(|)
  
  Given(/^there (?:is|are) (an?|-?\d+) (?:#{factory_matcher})$/) do |n|
    FactoryGirl.create_list factory_name, n
  end
end

If you have a factory named GuestUser, you could now use this step as follows:

Given there is a guest user
And there are 3 guest users

You could even get extra fancy and include FactoryGirl traits:

factory.defined_traits.each do |trait|
  trait_name = trait.name.to_s.humanize.downcase
  Given(/^there (?:is|are) (an?|-?\d+) (?:#{factory_matcher}) (?:that is |with |that are)?#{trait_name}$/) do |n|
    FactoryGirl.create_list factory.name, n, trait.name
  end
end

If your factory looks like this:

FactoryGirl.define do
  factory :guest_user do
    trait :stale do
      created_at { 6.weeks.ago }
    end
  end
end

…you could use the following step in you scenarios:

Given there are 2 guest users that are stale

I am not arguing that you should use elaborate Background sections for your scenarios to set up complex data sets. I do think these steps can help create readable exceptions to more generic, bigger setup steps.

Do observe that if you use these steps, you are automatically forced to write meaningful names for your factories and traits. I’ve found this effect very helpful, as they have the unintended side effect of also clarifying my unit tests.

4. Time travelling scenarios

Sometimes, your scenarios are dependent on a particular date and time. For example, you might develop a calendar system and want to test the colouring of “yesterday” – whatever that may be. Rails 4 ships with some good helper methods for stubbing Time (seeActiveSupport::Testing::TimeHelpers) and for earlier versions and non-Rails projects there’s Timecop. You can use these easily in a step:

Given(/^the current date is (.+)$/) do |time_string|
  travel_to(Time.parse(time_string))
end

So far, so good. We can use a step like Given the current date is 2014-04-23 and our tests run as if that is the current date. But we need to remember to travel back to the actual date and time, so as not to influence other tests. We might use Cucumber’s After hook:

After do
  travel_back
end

…but to more explicit, let’s use tags to indicate that this particular scenario uses time travelling:

After('@time_travel') do
  travel_back
end

Now we can write a scenario like:

@time_travel
Scenario: Time-dependent test
  Given the current date is 2014-04-23
  ...

5. Switch between multiple sessions

Capybara supports multiple sessions. You could use this to simulate logging in as two users at the same time, so that one user sees another user’s changes appearing in his browser in real time (just to name an example). The API is simple: just assign a session name:

Capybara.session_name = 'John'

Now you are in session “John”. Assign “Graham” and you’re in session “Graham”. Translating this to a Cucumber step is easy:

When(/^I am in (.*) browser$/) do |name|
  Capybara.session_name = name
end

But switching explicitly is kind of awkward, so we can use compound steps – where one step completely contains another:

When(/^(?!I am in)(.*(?= in)) in (.*) browser$/) do |other_step, name|
  step("I am in #{name} browser")
  step(other_step)
end

This step matches anything not starting with I am in (our original browser step) but ending with in <name> browser. Anything that comes before it, is called as another step. For example:

When I log in as admin in John's browser
And I log in as customer in Graham's browser

These two steps both run the I log in as <role> steps but in different Capybara sessions. I took this tip from Collective Idea.

  • ruby
  • cucumber
Arjan van der Gaag

Arjan van der Gaag

A thirtysomething software developer, historian and all-round geek. This is his blog about Ruby, Rails, Javascript, Git, CSS, software and the web. Back to all talks and articles?

Discuss

You cannot leave comments on my site, but you can always tweet questions or comments at me: @avdgaag.