Cucumber Step Definitions are teleportation devices, not methods

February 1, 2013 Michael Cucchi

Step definition hell. We’ve all heard of it. We’ve all experienced it. The question is, why?

This hell is borne from a simple, yet fundamental, misunderstanding.

When I first learned Cucumber, I instinctively thought of a step definition as a method. I could squint and imagine I was looking at a method. The regex looked roughly like a method “name”. The block arguments were basically like method arguments. The body of the block looked like a method body.

This led me to treat my step definitions like a basic unit of organization within my test suite. A typical step definition body might look like this:

Given /^I have created a widget named "([^"]+)$"$/ do |widget_name|
  visit widgets_path
  fill_in "Name", with: widget_name
  select "sortable", from: "Type"
  check "Fizzable"
  check "Buzzable"

  within(".widget_form .actions") do
    click_on "Submit"
  end

  within(".widget_form .confirmation") do
    choose "Widget Administrator", from: "Approver"
    click_on "Confirm"
  end
end

Then, I began calling one step definition from another step definition:

Given /^I configured a widget named "([^"]+)$"/ do |widget_name|
  step "Given I have created a widget named "#{widget_name}""
  step "Given I have changed the fizbuzz property of my widget "#{widget_name}" to "wuzbang""
  step "Given the widget administrator has approved the widget "#{widget_name}" configuration"
end

And then I nearly killed myself. Because it turns out that step definitions are not methods. Let’s start with the step definition method “name”. It’s not a method name. When you “call” it, you mix in the arguments with the “name”, making the “name” change depend on the arguments. This makes it nearly impossible to do something as simple as an automatic refactor/rename of these “methods” (unless you like writing really complicated regular expressions for find and replace), much less any more complicated refactorings.

Also, there’s a reason real method names are concise. It’s so that we can remember them. With a test suite of more than a couple feature files, you simply can not remember the names of cucumber steps with any satisfying degree of accuracy (which is the same reason you shouldn’t attempt to force your product owner to remember all the exact wordings of existing steps, and instead, let them write the same “step” many different ways).

If you pursue this path of treating step definitions like methods, you will create such a tangled mess that you’ll be left with little choice but to either abandon cucumber entirely or burn your cukes to the ground and start over.

Step Definitions are Teleportation Devices

The only way I’ve found to do sustainable acceptance testing (whether or not it’s cucumber, rspec feature specs, or minitest integration tests) is to create an underlying system of helper methods that represent actions in your application. For example, if you were building Twitter, I would expect to open up your acceptance testing suite and find a module or set of modules with methods that represent the Twitter application domain. That might look something like this:

module Helpers
  def authenticate(user)
    visit root_path
    fill_in "Username", with: user.username
    fill_in "Password", with: user.password
    submit
  end

  def tweet(message)
    visit tweets_path
    fill_in "Tweet", with: message
    #...
  end

  def reply(tweet, message)
    #...
  end

  def dm(message, recipient)
    #...
  end

  #... etc
end

A system of underlying helper methods like this make it really easy to spin up new features. It makes it really easy to let your product owner write the same step 5 different ways. Instead of tracking down the exact wording of the step already in the system and rewriting the feature file your product owner wrote, just take the feature “as is”, spin up new step definition and use your DSL to fill it out (creating any new DSL methods to match new concepts as you go).

If you want to make these module methods available to your cucumber step definitions, you can use the World method:

World Helpers

If you’re using RSpec request specs, use the RSpec configuration:

RSpec.configure do |c|
  c.include Helpers, type: :request
end

Then you’re left with step definitions that transport you from the feature file to your underlying DSL:

Feature: Tweet

  Scenario: Valid tweet
    Given Bob has authenticated
    When Bob submits a valid tweet between 1 and 140 characters
    Then his followers should receive his tweet
    And Bob should see his tweet in his timeline
Given /^I have authenticated$/ do
  authenticate bob
end

When /^Bob submits a valid tweet between 1 and 140 characters$/ do
  tweet valid_message
end

#...

That’s why I think of them as teleportation devices. They transport me from the written world (the feature file) to a world of code (my application’s acceptance DSL).

About the Author

Biography

More Content by Michael Cucchi
Previous
Introducing Simple BDD
Introducing Simple BDD

Simple BDD is a way to bring structured natural language BDD syntax into any test framework, but why is thi...

Next
Dealing with EXC_BAD_SELECTOR runtime crashes
Dealing with EXC_BAD_SELECTOR runtime crashes

One very powerful ability of ObjC is the dynamic runtime selector (SEL) that allows you to name a function ...

Enter curious. Exit smarter.

Learn More