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….