We hope to release Cucumber-Ruby 3.0.0 in the next few weeks. This major release has some exciting new features, but also some breaking changes.
I know upgrading to a version with breaking changes can be a hassle. My goal with this blog post is to convince you that an upgrade is worthwhile, and to make it as easy as possible.
Let's see what's changed.
Thanks for 7 or so dutiful years of service. Enjoy the retirement. Sorry, Transform
is no longer with Cucumber. Welcome its replacement - ParameterType
.
On the surface, ParameterType
is similar to Transform
- it lets you transform an argument from a string to another type before it's passed to the step definition.
Prior to 3.0.0 you might have defined a Transform
for integers:
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
assert(count.is_a?(Numeric))
# ...
end
In 3.0.0, int
conversion is built-in, so there is no need for that transform.
But what if we want to define our own, for a Person
type?
Before we'd just define a Transform
, and change the block parameter from name
to person
for good measure:
Transform /^([A-Z][a-z]+)$/ do |name|
Person.new(name)
end
Then /^a user, named '([^']+)', should have (\d+) followers$/ do |person, count|
assert(person.is_a?(Person))
end
In Cucumber-Ruby 3.0.0 we'd replace that Transform
with the following ParameterType
:
ParameterType(
name: 'person'
regexp: /[A-Z][a-z]+/,
transformer: -> (name) { Person.new(name) }
end
We also have to make the step definition use the same regexp as the ParameterType
. So you'd have to change this:
Then /^a user, named '([^']+)', should have (\d+) followers$/ do |person, count|
assert(person.is_a?(Person))
end
to this:
# <---------> same as parameter type's regexp
Then /^a user, named '([A-Z][a-z]+)', should have (\d+) followers$/ do |person, count|
assert(person.is_a?(Person))
end
Let's recap. A Transform
is replaced by a ParameterType
in two steps:
- Define a
ParameterType
with the same regexp as the Transform
- Make sure all step definitions using the transform have the same regexp in the
corresponding capture group.
Nested capture groups
If you have regular expressions with nested capture groups, the behaviour is slightly different. Pre-3.0.0 the number of capture groups had to be equal to the number of block parameters:
Given(/^I have a( trial)? subscription( with (\d+) days left)?$/) do |trial, with_days_left, days_left|
end
Cucumber 3.0.0 and beyond will expect only 2 parameters for the example above. It only considers the top level capture groups to be parameters, and not the nested ones.
So how would we get access to the days_left
value? With a ParameterType
of course!
ParameterType(
name: 'subscription',
regexp: /(trial )?subscription(?: with (\d+) days left)?/,
transformer: -> (trial, days_left) { Subscription.new(!!trial, days_left.nil? ? nil : days_left.to_i) }
)
The neat thing is that you can have capture groups in your parameter type's regexp
. Each capture group is passed to the transformer
. If you don't have any capture groups, the entire match of the regexp
is passed to the transformer
.
And now our step definition is simply:
Given('I have a {subscription}') do |subscription|
end
There is a bonus feature as well. If you now write an undefined step like this:
When I cancel my trial subscription with 2 days left
Cucumber knows you're talking about a subscription, and will suggest this snippet:
When('I cancel my {subscription}') do |subscription|
end
I think that is p r e t t y cool!
Sometimes your parameter types might have an overly general regexp
. In this case you might want to instruct Cucumber not to suggest it in snippets. You can do this by setting use_for_snippets
to false
:
ParameterType(
name: 'person',
regexp: /[\w]+/,
transformer: -> (name) { Person.new(name) },
use_for_snippets: false
)
Regular Expressions to Cucumber Expressions
As shown above, you can keep using regular expressions and you can leave them untouched unless you've been using them with transforms, or you've got nested capture groups.
Here are some tips about how to translate Regular Expressions to Cucumber Expressions:
Anchors
The gorgeous ^
and $
you love so much are no longer needed. Just get rid of them.
Capture Groups
All capture groups become Cucumber Expression output parameters. There are 4 built-in ones to choose from:
(\d+)
becomes {int}
- Whatever funky pattern you've been using for floats becomes
{float}
(\w+)
becomes {word}
(a word without quotes)
"([^"]+)"
becomes {string}
(anything between single or double quotes)
As we've seen above you can also define your own parameter types and use them as output parameters in your expressions.
Optional text
Optional text is just a way to make our language a little more flexible. We want to be able to say both 3 cucumbers
and 1 cucumber
, so we make the s
optional:
Given(/^I have (\d+) cucumbers? in my belly$/) do |count|
end
In Cucumber Expressions, this becomes:
Given('I have {int} cucumber(s) in my belly') do |count|
end
This use of parenthesis is closer to how people actually write.
The president (and his assistant) traveled by private jet
Alternation
Just like optional text, alternation is a way to make the language more flexible. We want to be able to say both 2 people
and 1 person
, so we use alternation:
Given(/^there (?:was|were) (\d+) (?:person|people) at the inauguration$/) do |count|
end
In Cucumber Expressions, this becomes:
Given('there was/were {int} person/people at the inauguration') do |count|
end
This use of a slash is closer to how people actually write.
Each guest must present his/her ticket prior to entry
I hope you're curious to try out the new features in Cucumber-Ruby 3.0.0, and that this blog post will make it easier for you to upgrade.
If you need help upgrading, there are always many people ready to help in the support channels.