Cucumber: Step Argument Transforms

Cucumber is a great tool that lets you create something akin to a personalized programming language for testing. If you haven’t heard of it yet, refer to previous posts on basic and advanced cucumber. While I love what Cucumber lets you do, up until now a lack of modularity within step definitions has been the elephant in the room. Dave Astels and I were lucky enough to stumble upon a neat solution to this while pairing recently, so let’s take a more specific look at the problem and solution. Note that at the time of writing the feature is only available in the trunk Git version of Cucumber, but expect a release soon.

Problem: Step Definition Arguments Captured as Strings

As you write step definitions that capture variable data in the provided regex, you’ll remain happy if the target data is naturally represented as a string. The following example uses DataMapper as its ORM and dm-sweatshop (found in dm-more) for fixture factories:

# step definition file
Given /^a new user with username (.+)$/ do |username|
  @user = User.make(:username => username)
end

Then /^the user is valid$/ do
  @user.should be_valid
end

# feature file
Scenario: valid username
  Given a new user with username larrytheliquid
  Then the user is valid

However, there are many occasions where the natural data-type for a captured argument may be something other than a string:

# step definition file
Given /^a new user with age (\d+)$/ do |age_string|
  @user = User.make(:age => age_string.to_i)
end

Then /^the user is valid$/ do
  @user.should be_valid
end

# feature file
Scenario: valid age
  Given a new user with age 22
  Then the user is valid

The key thing to notice is that it was necessary to convert age_string to an integer because the concept of age is naturally represented here as an integer. While the conversion does not look like much work now, the actual problem lies in modularity. If we would like to refer to age in any subsequent step definitions, we must duplicate the code to coerce the string everywhere… certainly not DRY.

The code we’ve seen so far is able to get away with going from captured argument string, to the argument with its natural data-type, to an ORM instance representation, without too much pain. This is possible because the structure of the scenarios being written is fairly simple and setup can be performed in a Given that creates an instance variable to be used in a Then. However, there are more complex scenarios where this pattern is not possible.

# step definition file
Given /^a customer user named (\w+)$/ do |username|
  User.gen(:customer, :username => username)
end

Given /^a support user named (\w+)$/ do |username|
  User.gen(:support, :username => username)
end

When /^user (\w+) is assigned user (\w+)$/ do |support_username, customer_username|
  support_user = User.first(:username => support_username)
  customer_user = User.first(:username => customer_username)
  support_user.assign(customer_user)
end

Then /^user (\w+) should be in user (\w+)'s work queue$/ do |customer_username, support_username|
  support_user = User.first(:username => support_username)
  customer_user = User.first(:username => customer_username)
  support_user.work_queue.should include(customer_user)
end

# feature file
Scenario: support assigned a customer
  Given a support user named stoltini
  And a customer user named larrytheliquid
  When user stoltini is assigned user larrytheliquid
  Then user larrytheliquid should be in user stoltini's work queue

The example above needs to reference specific users in both a When and a Then, and cannot rely upon a single instance variable that sets up state in a Given. The unfortunate result is some pretty nasty duplication of the ORM finder code to lookup each user by their respective usernames in the database.

Solution: Step Argument Transforms

The historical problem with Cucumber has always been that it was restricted to yielding strings as step definition arguments, but this is no longer the case. With a new Transform method, we are able to register regular expressions with Cucumber that it will check against arguments before they are yielded to step definitions. In addition to a regex, Transform expects a block that will be passed the raw argument, and whose return value will be used in place of it.

First, lets revisit our original modularity problem in the age example:

# support file
Transform /^age \d+$/ do |step_arg|
  /(\d+)$/.match(step_arg) \[0\].to_i
end

# step definition file
Given /^a new user with (age \d+)$/ do |age|
  @user = User.make(:age => age)
end

Then /^the user is valid$/ do
  @user.should be_valid
end

If you look at the Given, you’ll see that we expanded the capture group to include “age” as valuable contextual information. With step argument transforms, such contextual information is important to avoid overly general transforms that affect every argument.

The first argument to Transform uses a regex that anchors the beginning and end. This means that we will only match that specific entire string, rather than accidentally matching other step arguments that happen to contain a partial piece of our regex (of course, you could have more general versions if you wanted to, just tread carefully).

If a registered transform matches an argument of a step definition, that argument will be passed to the block supplied with the transform definition. In the Transform example above, we anchor at the end and just capture the digit, because we already know the structure of our input based on the initial match.

After pulling the information we want out of the match group, we apply our transform, to_i, which is yielded to the Given as the variable age. Note that we chose the name age in our new Given instead of age_string because we are expecting the transform to be applied. Most importantly, any other step definition that captures a group of the form (age \d+) will happily transform age into its natural type, keeping our code nice and DRY. Let’s see how step argument transforms change our previous more complex scenario.

# support file
Transform /^user \w+$/ do |step_arg|
  User.first :username => /(\w+)$/.match(step_arg) \[0\]
end

# step definition file
Given /^a customer user named (\w+)$/ do |username|
  User.gen(:customer, :username => username)
end

Given /^a support user named (\w+)$/ do |username|
  User.gen(:support, :username => username)
end

When /^(user \w+) is assigned (user \w+)$/ do |support_user, customer_user|
  support_user.assign(customer_user)
end

Then /^(user \w+) should be in (user \w+)'s work queue$/ do |customer_user, support_user|
  support_user.work_queue.should include(customer_user)
end

Here we use a similar strategy to capture groups of the form (user \w+). The transform applied looks up the user by their username and returns a DataMapper instance. The neat thing is that we can reuse our capture group across multiple different step definitions (the When and the Then), and the more involved duplicated boilerplate code gets packed away in the call to Transform.

Tips and Tricks

Scenario outlines and example tables are really cool features of Cucumber that let you specify a lot of different permutations of data in a compact way. However, the feature is also somewhat limited, because it can only yield string data. With step argument transforms, you’ll find yourself using the awesome tables more because duplicated transform code is removed so there is less friction to write additional step definitions.

# feature file
Scenario Outline: username validity
  Given a new user with age <age>
  Then the user is <validity>

Scenarios: valid
| age | validity |
| 18  | valid    |
| 21  | valid    |
| 49  | valid    |
| 120 | valid    |
Scenarios: invalid
| age | validity|
| 0   | invalid |
| 1   | invalid |
| 12  | invalid |
| 17  | invalid |

#... plus different steps using age

Sometimes it may be more convenient to pass a string version of a regex to Transform, so this is supported. Below is an example where the goal is to test properties of a Unix system. Any capture groups that contain path followed by a Unix path are desired to be expanded into their absolute system filepath. The UNIX_PATH_CAPTURE pattern is designed to be regex-interpolated into other regex capture groups, so it is defined as a string to prevent unintentional use as a standalone regex.

# support file
UNIX_PATH_CAPTURE = 'path (?:\w+|\/|\.|-|~)+'

Transform UNIX_PATH_CAPTURE do |step_arg|
  File.expand_path /^path (.*)/.match(step_arg) \[0\]
end

To avoid overly confusing dependencies, a step argument may only be transformed once. The Transform defined last gets matching order precedence over previously defined transforms, giving you the ability to “override” previous transforms. As a rule of thumb, define general transforms first and get more specific last. More importantly, appropriately including contextual data in capture groups prevents potentially unexpected transforms.

Conclusion

Cucumber has been a fantastic and innovative tool thus far. With step argument transforms, another bit of frustration is removed and your step definitions stay DRY.

As a final note, I’d like to point out how awesome it was to hack out the first version of this with Dave Astels while pairing, test-driven, and in less than an hour… given his BDD/RSpec/Cucumber background =) As mentioned before, we were pairing and ran into a problem that unearthed this feature. Before we knew it the console flashed from red to green.

Happy hacking!