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 makes 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: , , , , No Comments


Moving your application to Heroku

Recently, I moved an app from Rack Space to Heroku. Everything went well except for a few hangups.

Migrating your database.

My app currently used MySQL and, as you probably know, Heroku love Postgres. So how do we get this data up to Heroku?

One option would be to use the taps gem to import the MySQL data into Postgres. But that didn’t work so well for me. The gem seemed to fail arbitrarily, leaving me unsure about the fidelity of my data.

Another option, use Heroku’s Amazon RDS addon. It was a little painful to setup but not terrible and there’s plenty of good documentation.

Fixing bad encodings.

Once I got my database migrated, I noticed a new issue. Little �’s were peppered throughout my copy. That’s a bad encoding since, apparently, I’m using a different version of MySQL. So I wrote a little script to replace bad encodings with HTML entities.

It’s not exhaustive, but it’s solid and should be easy to adapt if needed.

Things to watch out for.

  • Does your app use SSL? If so, plan ahead since that can take a few days to work out.
  • If you’re using sendgrid, be sure to choose a plan that won’t cap out and start throwing errors.
  • Don’t forget to set both your MX and A records.
  • Don’t forget to setup New Relic and Hoptoad before switching over. You’ll probably encounter some errors and it’s better to know about them.

Tags: , , , No Comments


Adding default configuration options to Paperclip

Using paperclip, you’ll often put something like this in your models:

      has_attached_file :article_text,
        :storage => :s3,
        :s3_credentials => {
          :bucket => ENV['AS3_BUCKET'],
          :access_key_id => ENV['AS3_ACCESS_KEY_ID'],
          :secret_access_key => ENV['AS3_SECRET_ACCESS_KEY']
        }
       :s3_permissions => :private,
       :processors => [:manuscript_converter],
       :path => "manuscript/:id/:uploaded_manuscript_filename"
    

Many of these settings will be repeated in every paperclip’d model. You can easily remove that duplication by adding an initializer. In config/initializers/paperclip_defaults.rb:

      Paperclip::Attachment.default_options.merge!(
        :storage => :s3,
        :s3_credentials => {
          :bucket => ENV['AS3_BUCKET'],
          :access_key_id => ENV['AS3_ACCESS_KEY_ID'],
          :secret_access_key => ENV['AS3_SECRET_ACCESS_KEY']
        }
      )
    

And now you can remove those options from all your models – separating the parts that change from the parts that stay the same.

I had trouble finding this online and had to dig through the source code to figure it out. So maybe it will help someone else save a little time.

Tags: , No Comments


« Older Entries |