Cucumber: Step Argument Transforms

By Larry Diehl | September 14th, 2009 at 10:09AM

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 this post:
  • email
  • Digg
  • del.icio.us
  • Reddit
  • Slashdot
  • StumbleUpon
  • Technorati
  • Twitter
  • Google Bookmarks
  • Facebook
  • LinkedIn
  • Print
Popularity: 6% |
Post rating: 1 vote, average: 5.00 out of 51 vote, average: 5.00 out of 51 vote, average: 5.00 out of 51 vote, average: 5.00 out of 51 vote, average: 5.00 out of 5
Loading ... Loading ...

This website uses IntenseDebate comments, but they are not currently loaded because either your browser doesn't support JavaScript, or they didn't load fast enough.

18 Responses to “Cucumber: Step Argument Transforms”

  1. JJM JJM says:

    Larry,

    forgive if this sounds naive (I am a novice in both ruby, and cucumber), but why didn't you guys favor an implementation, where you would "overload" the the regex result and allows something like this:

    a) trivial case:
    Given /^a new user with age (d+)$/ do | age.to_i |

    b) non-trivial case:
    When /^user (w+) is assigned user (w+)$/ do |support_user.to_user, customer_user.to_user|

    # framework, deep inside cucumber
    overload magic for String(????) or RegexResult(????)

    #supporting file
    def to_user
    User.find_by_name….
    end

    Or is this something that is not possible in ruby/rails?? Again, sorry if this sounds all to naive, but just from a convention/readability point of view, I thought transforming the result itself seems easer to understand?

    Just saying … JJ

    • Hi JJM,

      If what you suggested were possible, then you would still not keep code DRY because you would need to duplicate the various #to_whatever calls in each step definition.

      In Ruby, you can extend the String class to add extra #to_whatever methods like you suggested, but you can't make method calls inside the block parameters like you showed.

  2. For simple cases where you do need to parse the entire step argument, you can include capture groups that get yielded as arguments instead:

    Transform /^age (\\d+)$/ do |age_string|
    age_string.to_i
    end

    Transform /^user (\\w+)$/ do |username|
    User.first :username => username
    end

    Also, as of Cucumber 0.3.101 step argument transforms are in the official release!

  3. Matt Parker Matt Parker says:

    great work guys!!! this is exactly what I've been needing.

  4. Is there a way that a Transform can fail if it detects that it was not the correct match and allow a Transform further down the chain to try? Maybe by raising a specific exception.

  5. Eric Eric says:

    I have a similar but different problem. I have various ways to specify users and various ways to specify people, and various kinds of steps where I use either one (I have many more needs like this as well). I would like to have a each scenario use any combination in a single Given statement. I'd like something like the following:

    TokenTransform "PERSON", /^a young child$/ do
    …code that creates a young child person and returns it…
    end

    TokenTransform "PERSON", /^an elderly person$/ do
    …code that creates an elderly person and returns it…
    end

    TokenTransform "USER", /^a guest user$/ do
    …code that creates a guest user and returns it…
    end

    TokenTransform "USER", /^a user with role (w+)$/ do | roleString |
    …code that creates a user with a certain role and returns it…
    end

    Given /^(PERSON) added by (USER)$/ do | person, user |
    … (code that uses person and user as their corresponding types, already converted from a string)…
    end

    Is there a way to do this already? If not, how might something like this look like if it were added to Cucumber?

    • Heya Eric,

      I'm not sure if I understand your situation exactly, let me know if this solves it:

      # support file
      Transform /^new young child (\\w+)$ do |first_name|
      Person.gen(:child, :first_name => first_name)
      end

      Transform /^new guest user (\\w+)$/ do |username|
      User.gen(:guest, :username => username)
      end

      Transform /^person (\\w+)$/ do |first_name|
      Person.first :first_name => first_name
      end

      Transform /^user (\\w+)$/ do |username|
      User.first :username => username
      end

      # step definition file
      Given /^a (\\w+) added by (\\w+)$/ do | person, user |
      user.add person
      end

      Then /^(user \\w+) has a referral for (person \\w+)$/ do |user, person |
      user.should have_referral_for(person)
      end

      # feature file
      Given a new young child Mike added by new guest user coolio
      Then user coolio has a referral for person Mike

  6. Eric Eric says:

    Ok, so from from you're saying, it seems that the regular expression defined for a Transform does not have to appear verbatim in the regular expression of the Given/Then/When. That was not clear from reading the original description. So, that means that every single captured group (anything in parens) is potentially checked against every defined Transform. Is that right?

  7. Corey Haines Corey Haines says:

    Is there a way to transform table data coming in. I'm working on some practice features for Conway's Game of Life (http://github.com/coreyhaines/practice_game_of_li... and I'd like to be able to use a transform to capture the tables.

  8. nruth nruth says:

    For more dry fun with cucumber models take a look at Ian White's pickle plugin, which lets you write steps like

    Given a user exists with name: "Frank"
    And I go to the user's page
    Then I should see "name: Frank"

    without writing any steps: it fits in with machinist, factorygirl, or just active record.

    http://ianwhite.github.com/pickle/

    It's an ongoing project, but it works, and I find it very useful in speeding up feature writing.

  9. Pete Wright Pete Wright says:

    Love it Larry – extremely useful and informative :) Nice to see this on the Cucumber docs wiki now too.

Leave a Reply