You're cuking it right

This post is inspired by “You’re cuking it wrong” by Jonas Nicklas, and Elisabeth Hendrickson’s talk on writing clear acceptance tests during GoGaRuCo 2010.

Here are some quick lessons in real-world Cucumber story writing.

Avoid ‘within’ steps with CSS selectors

If you’ve read the aforementioned blog post by Jonas, you got a pretty good idea why this is wrong:

Then I should see "Hello world" within "h1"

Acceptance tests should be readable by non-developers involved in the project. Try to express this step in a sentence which you would use while talking to a human:

Then I should see "Hello world" in the title

Of course, this is a custom step. In order to reduce clutter while implementing custom steps under the hood (i.e. on the Ruby side), I’ve devised this scheme for mapping step sentence endings to CSS selectors:

# within_steps.rb
{
  'in the title' => 'h1, h2, h3',
  'as a movie title in the results' => 'ol.movies h1',
  'in a button' => 'button, input[type=submit]',
  'in the navigation' => 'nav'
}.
each do |within, selector|
  Then /^(.+) #{within}$/ do |step|
    with_scope(selector) do
      Then step
    end
  end
end

This short snippet constructs a few regular expressions that allow you to write any of the existing steps and end them with “in the title”, “in a button” or scope them to a certain selector.

Dealing with multiple items on a page

When a web page lists multiple items on the same page—e.g. movies, books, search results—you need fine-grained control over which element your steps are interacting with, but you also need a way to express that in a natural way. I tackled this problem using a variation of the technique described previously.

Here is the HTML structure of movies returned by a search:

<ol class="movies">
  <li class="movie">
    <a href="...">
      <img src="...">
      <h1>The Terminator</h1>
      <span class="year">(<time>1984</time>)</span>
    </a>
    <aside>...</aside>
  </li>
  
  <li class="movie">...</li>
  ...
</ol>

I scope to a specific movie by referencing its title:

Then I should see "(1984)" for the movie "The Terminator"
And I should see 'This movie is in your "to watch" list.' for that movie
But I should not see a "Want to watch" button for that movie

# implementation:
When /^(.+) for the movie "([^"]+)"$/ do |step, title|
  @last_movie_title = title
  within ".movie:has(a h1:contains('#{title}'))" do
    When step
  end
end

When /^(.+) for that movie$/ do |step|
  raise "no last movie" if @last_movie_title.blank?
  When %(#{step} for the movie "#{@last_movie_title}")
end

The “for the movie …” regular expression will match any step that ends with that expression, scope it to a movie that has the given string in the title, and execute the step normally. The movie title is stored in an instance variable that lets me use “for that movie” steps later on.

Logging in without going trough the login form process

In a typical application, most of the steps depend on a certain user being logged in. Some of the stories need to describe the login process in detail, but most don’t. Filling out the login form for every scenario across all your stories creates unnecessary overhead in runtime.

To skip the login process almost completely, I created a ‘backdoor’ route that logs any user by their username instantly:

Given I am logged in as @mislav
  # implementation
  Given /^I am logged in as @(\w+)$/ do |username|
    visit "/login/#{username}"
    @current_user = User.find_by_login(username)
  end

  # config/routes.rb
  if Rails.env.cucumber?
    map.login_backdoor '/login/:username',
      :controller => 'sessions', :action => 'backdoor'
  end

  # in the controller
  class SessionsController < ApplicationController

    # for cucumber testing only
    def backdoor
      logout_killing_session!
      self.current_user = User.find_by_login!(params[:username])
      head :ok
    end

  end

Have your steps support multiple users doing the described action

If you find yourself repeating the same action for multiple users, or a single user doing the same action on multiple items, consider implementing your steps in a way in which they support these plural forms. An example, here is an overly verbose background section, refactored:

# before (very bad):
Background: 
  Given the following confirmed users exist
    | login  | locale   |
    | balint | it       |
    | pablo  | es       |
    | james  | en       |
  Given the following conversations exist in the project "Testing" owned by mislav
    | name     | body     |
    | Politics | Discuss! |
  Given "balint" is watching the conversation "Politics"
  Given "pablo" is watching the conversation "Politics"
  Given "james" is watching the conversation "Politics"

# after (better):
Background: 
  Given a project with users @balint, @pablo, @mislav and @james
  And @balint has his locale set to Italian
  And @pablo has his locale set to Spanish
  ...
  Given I started a conversation named "Politics"
  And the conversation "Politics" is watched by @balint, @pablo and @james

To support lists like “@balint, @pablo and @james”, I implement the steps this way:

Given(/^a project with users? (.+)$/) do |users|
  @current_project = Factory(:project)

  each_user(users, true) do |user|
    # make the user a member of the @current_project
  end
end

The each_user helper parses out any string in search of usernames, finds those users and yields the block for each one:

# features/support/usernames.rb
module ManyUsernames
  def each_user(usernames, factory = false)
    usernames.scan(/(?:^|\W)@(\w+)/).flatten.each do |name|
      user = User.find_by_login(name)
  
      unless user
        if factory
          Factory.create(:user, :login => name)
        else
          raise "can't find user with login '#{name}'" unless user
        end
      end
  
      yield user
    end
  end
end

World(ManyUsernames)

If you take care while making the regular expression, you can easily have it also support the singular form. The project factory step supports the single member form:

Given a project with user @mislav

More reading