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