Ten tips for writing better Cucumber steps

1. Use flexible pluralization.

Add a ? immediately following the pluralized word:

        Then /^the users? should receive an email$/ do
          # ...
        end
    

The ? specifies that your looking for zero or more of the proceeding character. So the above example will capture both user and users.

2. Use non-capturing groups to help steps read naturally.

You can create non-capturing groups by adding a ?: to the beginning of a otherwise normal group (e.g. (?:some text) rather than (some text)). This is treated exactly like a normal group except that the result will is not captured and thus not passed as an argument to your step definition. This often useful in conjunction with alternation:

        And /^once the files? (?:have|has) finished processing$/ do
          # ...
        end
    

Or another pattern I use regularly:

        When /^(?:I|they) create a profile$/ do
          # ...
        end
    

3. Consolidate step definitions by capturing optional groups.

Often I find myself writing essentially the same step with both positive and negative assertions. You can remove this duplication by capturing an optional group:

        Then /^I should( not)? see the following columns: "([^"]*)"$/ do |negate, columns|
          within('table thead tr') do
            columns.split(', ').each do |column|
              negate ? page.should_not(have_content(column)) : page.should(have_content(column))
            end
          end
        end
    

Here we’re capturing an optional group (note ( not)? using the ? mentioned above). We then pass that into our step as the negate variable which we can use to write conditional assertions.

4. Not all matched phrases need to be surrounded by quotes.

Don’t assume that matched phrases must (or should) be enclosed in double quotes. Often quotes are a good idea. They make it visually clear which phrases will be passed to the step definition. For example:

        Given I have "potatoes" in my cart
    

That’s reasonable. The quotes highlight the parts that change without hurting readability. But sometimes, quotes are just poor style:

        Given I have "7" items in my cart
    

It should be pretty obvious that the number is variable. The quotes add nothing except noise. A better step would read:

        Then /^I have (\d+) items? in my cart$/ do |item_count|
          item_count.to_i.times { ... }
        end
    

5. Use transforms to make smarter, DRYer regular expressions.

There’s an opportunity to refactor the previous example. Do you see it? Cucumber passes everything as a string, so you must remember to convert types within your step definition (e.g. above we use to_i to transform item_count into a proper integer). That’s annoying and easy to forget.

Fortunately, Cucumber gives us a way to avoid this peskiness by using Transform:

        CAPTURE_A_NUMBER = Transform /^\d+$/ do |number|
          number.to_i
        end
    

And we can use this in our steps:

        Then /^I have (#{CAPTURE_A_NUMBER}) items? in my cart$/ do |item_count|
          item_count.times { ... }
        end
    

Not only have we removed the need to call to_i but we’ve also moved our regex into a reusable constant.

6. Define methods to DRY up your step definitions.

Sometimes it’s a good idea to remove duplication by defining methods. For example, it’s common to define a current_user method:

        def current_user
          User.find_by_email('current_user@example.com')
        end
    

Similarly, if you find yourself wrapping several steps with the same logic, you might consider making a helper method. On Scholastica, we test a lot of lightboxes using of this method:

        def within_lightbox(opts = {sleep: 0} )
          sleep(opts[:sleep])
          within_frame("prettyPhotoIframe") { yield }
        end
    

And our step definitions stay nice and clean:

        Then /^some stuff should be visible in the lightbox$/ do
          within_lightbox { page.should have_content('Some Stuff') }
        end
    

By convention, these methods should live in features/support/world_extensions.rb and be included in the Cucumber World module. But keep in mind this is a tradeoff: you’re removing duplication but adding indirection. You should be reluctant to define methods until the code makes it very obvious that it’s a good idea.

7. Use steps within steps.

Sometimes it’s useful to call steps within steps. Another example from Scholastica:

        When /^the request for an expedited decision should be canceled$/ do
          manuscript.should_not be_expedited
          step %{"#{current_user.email}" should receive an email with subject "Expedited decision request canceled"}
        end
    

But don’t go crazy, it’s better to use the capybara methods directly when possible:

        When /^I update the email template to read "([^"]*)"$/ do |text|
          fill_in("email_template[text], with: text)
          click_button("Save changes and close")
          # Rather than...
          # step %{I fill in "email_template[text]" with "#{text}"}
          # step %{I press "Save changes and close"}
        end
    

8. Improve readability with unanchored regular expressions.

Most step definitions look something like:

        Given /^I am an admin user$/ do |item_count|
          # ...
        end
    

Note we’re using ^ and $ to anchor our regex to the start and end of the captured string. This ensures the regular expression exactly matches “I am an admin user” (i.e. allows no additional words at the beginning or end of the step). Most of the time, this is exactly what want.

Occasionally, however, it makes sense to omit the final $. Take this step for example:

        Then /^wait (\d+) seconds/ do |seconds|
          sleep(seconds.to_i)
        end
    

Now you can use this definition to write flexible, expressive steps:

        Then wait 2 seconds for the revenue statistics to finish loading
        Then wait 5 seconds while the document is converted
    

9. When your steps must include data, use tables.

Generally, steps should be human readable and that means they shouldn’t include loads of cryptic data. But sometimes, you have no other choice. In those cases, use tables to clearly represent the data:

        Given "Frankie's Hams" are selling for $25:
        And the following orders have been placed:
          | buyer email      | quantity |
          | eddy@example.com | 3        |
          | matt@example.com | 2        |
    

Using tables within your step definitions can get a bit tricky. I use this helper method but you should really read the relevant source code and figure out what makes sense for your application.

10. Don’t get carried away and spoil a good thing.

As you use these tips, remember that tests should favor clarity over cleverness. In other words, if you’re removing a small amount of duplication but adding a lot of complexity, that’s a poor tradeoff. Here’s an example from Scholastica:

        Given /^(?:I|they) have opened (?:a|an) ([^\s]+) invitation$/ do |invitation_type|
          invitation = Factory("#{invitation_type}_invitation".to_sym, recipient: current_user, to: current_user.email)
          reset_mailer
          eval "ApplicationMailer.invite_#{invitation_type}(invitation).deliver"
          open_email(current_user.email)
        end
    

This steps allows us to write both Given I have opened a reviewer invitation and Given I have opened an editor invitation. I’d say this step is borderline too complex (lots of interpolation and an eval). Maybe it would be better to just write two separate steps? Remember no one is testing your tests so don’t fuck around. A little bit of duplication is not the end of the world.

Tags: , , , ,

3 Comments

Your thoughts?

Preview