Be Genius

me

Bo Jeanes

I am an Australian full-stack developer living and working in San Francisco. I strongly advocate open-source software and the community around it. I am a tool maker. I currently work primarily in Ruby and Rails but do as much Clojure, Go, and Javascript as I can.

Selector-Free Cucumber Scenarios

(a Serbo-Croation translation by Anja Skrbaa is available here)

I’ve been using Cucumber since pretty much the first day I heard about it. I’ve worked on a lot of projects that have relied on it’s presence for reliable development. Therefore, I’ve put a lot of effort into perfecting my Cucumber infrastructure to make this fantastic tool even better. I’m going to share one such morsel of code that makes developing with Cucumber even greater.

The Problem

I’ve worked on a lot of web applications and, as I’m sure many of you know, quite often the development of a web application is focussed on the functionality foremost, and the interface and style is incorporated later. It may be that the client doesn’t yet know the feel they want for their project or that they want to focus their budget towards prototyping the application first.

This is fine, except for the fact that changing the HTML and CSS of a web application after a lot of functionality has been developed is a fantastic way to break all your integration tests.

This is particularly true if you have scenarios like the following contrived one:

When I fill in "Username" with "bjeanes" within ".main-panel form#signup-form"
And I press "Sign up!" within ".main-panel form#signup-form"
Then I should see "You have successfully signed up" within ".flash.notice"

The problem is, of course, that designers might change the HTML that used to be .main-panel form#signup-form into something sexier and more semantic.

The Solution

This problem is not unlike an already-solved one; we’ve all moved away from hardcoding URLs like "/users/new" into our views and Cucumber scenarios and replacing them with new_user_path and the signup page, respectively.

So why not apply the same formula that paths.rb uses for removing URLs from scenarios to our situation with selectors?

Here’s what I add to all new projects:

# I'm in features/step_definitions/web_ext_steps.rb

When /^(.*) within ([^:"]+)$/ do |step, scope|
  with_scope(selector_for(scope)) do
    When step
  end
end

# Multi-line version of above
When /^(.*) within ([^:"]+):$/ do |step, scope, table_or_string|
  with_scope(selector_for(scope)) do
    When "#{step}:", table_or_string
  end
end  

And:

# I'm in features/support/selectors.rb

module HtmlSelectorsHelper
  def selector_for(scope)
    case scope
    when /the body/
      "html > body"
    else
      raise "Can't find mapping from \"#{scope}\" to a selector.\n" +
        "Now, go and add a mapping in #{__FILE__}"
    end
  end
end

World(HtmlSelectorsHelper)

Applying the Solution

My previous example of the flawed Cucumber scenario now becomes:

When I fill in "Username" with "bjeanes" within the sign up form
And I press "Sign up!" within the sign up form
Then I should see "You have successfully signed up" within the notice flash

And the selectors.rb case statement gets the following additions:

case
  # ...

  when /the sign up form/
    ".main-panel form#signup-form"
  when /the (notice|error|info) flash/
    ".flash.#{$1}"

  # ...
end

Notice how the scenario now identifies things on our page by their semantic identifiers, not by brittle CSS or XPath locations which are prone to change. As a bonus, now if when they do change, the paths only need to be updated in a single location in our Cucumber test suite!

Patching Cucumber

I feel pretty strongly that CSS and XPath don’t belong in our feature files because not only does it encourage brittle tests (as shown above), but also because those selectors are entirely irrelevant to end users, and that’s kind of the main point of using a natural language DSL to describe our integration tests, i.e. putting on the user shoes.

I’d really like to patch this back into Cucumber, and I entirely plan to do so, providing I get the time.

I got the time, and here is my pull request to have it merged.

Expanding the Solution

You’ll note my HtmlSelectorsHelper module only accommodates CSS selectors. That’s only because I have never needed XPath in this context. It’d be very simple to modify my examples to do so, though, with a combination of multiple return values and a splat. That’s an exercise for the reader (or me if I end up patching Cucumber).

Final Word

I apologise for the length of this article, but I congratulate you for making it all the way through it!

I now have so many blog post ideas lined up that I’ve had to create a new category in Things.app just to hold them all. This means that I’ll be striving to get a few more posts done and out the door in the next few weeks, including a performance comparison of different data encapsulations for web application APIs on the iPhone (i.e. is it better to use Plists, JSON, or XML?) and a post on why I think there should be 8 RESTful actions, not the 7 that Rails prescribes by default.

Comments

  1. What selectors do you tend to use to target form elements?

    I'm always torn between using labels such as "First Name" or the input ID "user_first_name" both of which have a tendency to change over time.

    Thanks for the write up.

    Scott

    by Scott Harvey on
  2. Scott, I always use label names purely because is what the user sees and it represents the semantic point of the field in stories and to the user.

    Form labels shouldn't change that much, and I've ensured that in the past by telling designers that they can't change the label of a form without prior discussion. It's easy for a designer not to change a label value, but hard for them not to change the classes, IDs, and elements that are used.

    Also, if you are using Rails' form helpers, I can't think of a reason why input IDs like "user_first_name" should ever change unless you are moving the forms around so much (such as nesting them in another form) that the whole scenario will have to be re-worked anyway.

    by Bo Jeanes on
  3. I was considering writing something like this, but this is 100x cleaner than what I was thinking of. Awesome!

    by modsognir on
  4. I wrote a post on this (and Cucumber in general) the other day, but your explanation is much more succinct (and has better examples). So, I've put a link in mine referring to your post.

    by Jason Stirk on
  5. Jason,

    That's very interesting that we both came up with pretty much identical solutions to the same problem. I think your post raises some very important points that I only glossed over at the end of my article. I.e. the philosophical reasons that features should be written cleanly (so users/clients can understand them) as opposed to just the technical reasons.

    Every one should read Jason's post too for re-enforcement of good cucumber practices.

    by Bo Jeanes on
  6. Here is another good post to read on the topic that I only found this morning but a lot of people seem to be talking about

    by Bo Jeanes on
  7. Nice stuff...have been thinking about this for a while, the in house designers I have are a very fickle bunch, always changing, breaking stuff.

    by Jonathan Clarke on
  8. For another variation on this theme checkout

    http://github.com/icaruswings/gizmo/wiki/Cucumber

    I posted a reply to you rails-oceana post

    :)

    by Steven Holloway on
  9. I agree 100% that Cucumber specs should be free of XPath, CSS selectors, and other implementation details. Nice work!

    I particularly like the general-purpose "within" steps, that delegate to other steps. Way cool.

    It occurs to me that you might want the same element description (e.g. "the signup form") to mean map to different selectors on different pages; have you given any thought to how you'd do that?

    by Mike Williams on
  10. Mike, you could keep a map of the necessary details keyed by the request path of each page where "the signup form" might mean different things....

    by Ben Hughes on

You need to login with GitHub in order to comment.