Skip to content
This repository was archived by the owner on Nov 16, 2018. It is now read-only.

Step Argument Transforms

burtlo edited this page Nov 14, 2010 · 28 revisions

Step argument transforms help your step definitions be more by DRY by allowing you to refactor common operations performed on the step definition arguments to a common operation. Before each step definition’s captured Regexp groups are yielded as arguments to the step definition block, an attempt is made to match them against registered Transform objects. If a capture value matches one of the Transform objects the result of the Transform block yields to step definition as the argument.

Registering a Transform

# features/step_definitions/user_steps.rb

Transform /^(-?\d+)^/ do |number|
  number.to_i
end
  • Uses the keyword Transform
  • Has a regular expression that is compared against all the captures of the step definition
  • Implements a block whose result is sent to the step definition block as the matching argument

Transforms in Execution

The following transform converts any step definition captures that match digits and converts them automatically to integers.

# features/step_definitions/user_steps.rb

Transform /^(-?\d+)^/ do |number|
  number.to_i
end

Then /^a user, named '([^']+)', should have (\d+) followers$/ do |name,count|
  # Without the transform your count object would be a string and not a number 
  count.should be_kind_of(Numeric)
  #...  
end

Transforming Tables

# features/table.feature
Feature: Users

  Scenario: Creating Users
    Given the following users:
      | user          | followers |
      | aslakhellesoy | 317       |
      | jberkel       | 51        |
      | bfaloona      | 4         |
      | mattwynne     | 33        |

(Re-)Mapping Headers and Columns

Table objects have two extremely useful methods: map_column! and map_headers!.

# features/step_definitions/user_steps.rb

Given /^the following customers$/ do |table|
  # Convert all headers to lower case symbol
  table.map_headers! {|header| header.downcase.to_sym }

  # Convert all user strings to Users
  table.map_column!(:user) {|user| User.find_by_name(user) }
  
  # Convert all the digits in the followers column to an Integer
  table.map_column!(:followers) {|count| count.to_i }

  table.hashes.each do |row|
    row[:user].followers = row[:followers]
  end
end

The Whole Table

A table like this one may occur again in a creation step or a validation step and Transforms will again help keep you DRY. Transforms can also be used with tables. A table transform is matched via a comma-delimited list of the column headers prefixed with ‘table:’

# features/step_definitions/user_steps.rb

Transform /^table:user,followers$/ do |table|
  table.map_headers! {|header| headers.downcase.to_sym }
  table.map_column!(:user) {|user| User.find_by_name(user) }
  table.map_column!(:followers) {|count| count.to_i }
  table
end

Given /^the following users$/ do |table|
  table.hashes.each do |row|
    row[:user].followers = row[:followers]
  end
end

Then /^I expect the following users$/ do |table|
  table.hashes.each do |row|
    row[:user].should_not be_nil
    row[:user].followers.should == row[:followers]
  end
end

Transform Wisdom

Transforms are powerful and it is important to take care how you implement them as mistakes with them can often introduce strange and unexpected behavior.

1. Transforms always require that you specify a block variable even if you do not specify any groups to capture.

With no capture groups specified the entire value is returned as a string to the block. This is contrast to how step definitions perform and can often trip you up. Cucumber will fail execution with a warning when you fail to specify at least one variable.

# Valid - without the capture group () specified
Transform /^-?\d+^/ do |number|
  number.to_i
end

# Invalid - Cucumber will raise an error when you execute the test suite
Transform /^-?\d+^/ do
  number.to_i
end

When you specify capture groups, each capture will be yielded to the block as you have likely come to expect from step definitions would expect.

# Converts the numeric abbreviation for KB/MB to the size 
Transform /^(\d+)\s?(KB|MB)$/ do |quantity,unit|
  quantity.to_i * (unit =~ /MB/ ? 1000000 : 1000)
end

2. Ensure that you specify the caret (^) and dollar sign ($) at the start and end of the regular expression.

The following transform, adds onto the integer conversion before it, treats an ‘a’ or ‘an’ as the integer value of 1. This may be useful in step definitions where you might want to say something like “a user” instead of “1 user”.

# Convert 'a' or 'an' to the integer value 1
Transform /^(an?|-?\d+)$/ do |amount|
  amount =~ /an?/ ? 1 : amount.to_i
end

However, removing the caret and dollar sign will allow this step definition to match ANY step definition captures that have the value a in it. This will likely wreak havoc as the value 1 starts appearing in a large set of your step definitions.

3. In step definition capture contextual to ensure that the correct transform is used.

Transform /^a user,? named '([^']+)',?$/ do |name|
  User.find_by_name(name)
end

# i.e. Then a user, named 'aslakhellesoy', should have 317 followers
Then /^(a user, named '(?:[^']+)',) should have (\d+) followers$/ do |name,count|
  user.should_not be_nil
  user.should be_kind_of(User)
  user.friends.should == count.to_i
end

Before using a transform, you would have likely only wanted to match the name, the value in single quotes, as a group However, matching only the name would likely open our transform up to a large number of arguments and we wouldn’t be assured that we were getting a user.

4. Register a Transform in the same file in which they are being employed.

If you find that you transform is used by multiple step definitions, move the transform to a file in the support directory (i.e. step_definitions/support/numeric_transforms.rb).

Further Examples

Larry Diehl at Engine Yard

Clone this wiki locally