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!
Share your thoughts with @engineyard on Twitter