cucumber for a story

a case study from rspec to cucumber

Ah, technology. Write a book with a chapter on testing programs in plain English, and they go and change the software the next morning. It would be enough to make a more timorous developer tip the entire bookshelf into the dustbin and give up on testing altogether.

But not us. You and I, we’re going strap Aslak’s guide to our side, wade in, and see just what it takes to whip a bunch of RSpec stories into shape for Cucumber. Specifically, we’re going to tackle the party planning web app from Chapter 10 of the book.

Renaming the files from .story to .feature and sticking them in a features/ directory is easy, right? No need to edit the contents of the file, unless you get weird parser errors—Cucumber is strangely unforgiving about really silly things like line breaks in comments. The examples for the book didn’t need any tweaking, but a couple of tests at work did. When in doubt, keep deleting lines (temporarily!) until you find the problem child.

Next, it’s time to free the step definitions from the blocks that enchain them. For the party-planning example in the book, we have steps_for :planning, :reviewing, :rsvp, and :email blocks. Here’s a tiny example. If this were one of our blocks:


steps_for :rsvp do
  Then 'I should see "$guest" in the list of $type' do |guest, type|
    want_attending = (type == 'partygoers')
    @party.responses(want_attending).should include(guest)
  end
end

...we’d ditch the first and last lines (steps_for... and end) and put the rest into features/step_definitions/rsvp_steps.rb). The other three groups of definitions get a similar treatment.

Next, we need to get rid of the Story Runner’s fuzzy string matchers and replace them with proper regular expressions. This only took a few seconds, even on the large body of tests we have at my day job; we just used a text editor search-and-replace to change Given ' to Given /^, and so on. (If your text editor is extra-clever, you can use a regex to, um, write your regex for you. Somewhere in there, there’s a Sup dawg joke just aching to get out.)

Next, we need to get rid of those dollar-variables inside, and replace them with good ol’ regex captures. All of the ones for this chapter worked with (.*)—the catch-all capture. For a chunk of tests we converted at work, there were a few places where Cucumber complained that a test step matched more than one regular expression. This kind of thing can happen when you want to match both of these:

When I frob the quux to 123

When I frob the quux to 123 furlongs

The naïve approach won’t work:


When /I frob the quux to (.*)/

When /I frob the quux to (.*) (.*)/

Because “123 furlongs” matches both (.*) and (.*) (.*). Cases like these call for more specific captures, either requiring numeric characters, or enclosing quotes, or optional matches, or some other way to tell them apart—for now. The Cucumber team are working on a way to resolve some ambiguous matches automatically. In the meantime, cleaning up regexes doesn’t hurt too bad.

After the changes we’ve done so far, the step definitions in rsvp_steps.rb would look like this excerpt:


Then /^I should see "(.*)" in the list of (.*)$/ do |guest, type|
  want_attending = (type == 'partygoers')
  @party.responses(want_attending).should include(guest)
end

Next up: the setup and teardown stuff. Cucumber provides Before and After hooks, but these run for every Scenario in our code. Some actions, like starting and stopping a web browser, are slower than typical setup code and should only be run one time. For these cases, Cucumber will run anything you put in features/support/env.rb just once—at launch time. Since this file can contain Ruby-style at_exit handlers, you can also put global teardown code in this file.

For this example, we need just one little tweak to the approach. The browser object needs to be available inside or test steps, so we can pass it into our Party support code. Cucumber’s World method will do the trick. Think of it as a Before hook with instance methods; if we define a browser method in a custom World, every test step in our suite will have access to the it.


class PartyWorld
  @@browser = Selenium::SeleniumDriver.new \
    'localhost', 4444, '*firefox', 'http://localhost:3000', 10000
  @@browser.start
  at_exit {@@browser.stop}

  def browser; @@browser end
end

World do
  PartyWorld.new
end

(Note that one test step uses the old @browser instance variable inside; it needs to be changed to use the browser method instead.)

The last holdover from our old RSpec-style stories is the with_steps_for ... run ... block that actually starts the tests. We don’t need this any more, since Cucumber comes with its own launcher that scans a directory for *.feature files and runs them:

cucumber features

Don’t forget to require 'spec/expectations' somewhere if you’re using the fancy RSpec matchers like should_have. See the code listings for the finished product.

Coming up next: I’ll explore using the lo-cal Bacon assertion framework instead of RSpec. Mmmm, Cucumber and Bacon….

blog comments powered by Disqus