In this recipe, will cover various kinds of tips for writing good, maintainable, and DRY Cucumber steps.
We will reuse the Rails application cucumber_bdd_how_to
that we've created in the Writing your first Hello World feature (Simple) recipe, so please cd
into that directory to get prepared.
In the following sections, a number of useful step tips will be introduced and covered exhaustively.
Let's imagine that we need to write a step that contains a singular or plural noun depending on its count:
When the user has 1 gift ... When the user has 5 gifts ...
Instead of implementing two similar step definitions, we can adopt a tip in Cucumber called Flexible Pluralization; the step to match the preceding steps is as follows:
When /^the user has (\d+) gifts?$/ do |num| p num.to_i end
Notice the
?
(question mark) appended togifts
; it means match zero or more of the proceeding character, and so the step definition will match bothgift
andgifts
.
Sometimes the plural of a noun is irregular, such as person/people, knife/knives. We cannot match them through flexible pluralization, and for these scenarios we need to adopt non-capturing groups, because Cucumber's step statements are eventually treated as regular expressions:
When there is 1 person in the meeting room When there are 8 people in the meeting room
We can define our step as follows:
When /^there (?:is|are) (\d+) (?:person|people) in the meeting room$/ do |num| p num.to_i end
By adding a
?:
before a normal group, the step will try to match one occurrence of the given word and will not pass the matched value into arguments. Non-capturing groups ensure Gherkin's good readability when dealing with singulars and plurals, and in a DRY manner since one generic step matches various kinds of styles.
Manually converting a parameter to an integer all the time would be really annoying! We are able to define a step argument transform rule within a step definition file that can be used by other steps:
Transform /^(-?\d+)$/ do |num| num.to_i end
The argument transform also supports tables! For example, we have a feature with table input as follows:
Given this Qatar billionaire has 39 billion And his wealth consists of the following major parts | Domain | Worth | | Oil | 21 | | Real Estate | 8 | | Financial | 6 | | Cargo | 4 |
We can then define a transform step to convert the table input into any data we want:
Transform /^table:domain,worth/ do |table| table.map_headers! { |header| header.downcase.to_sym } table.map_column!(:domain) { |domain| Domain.parse(domain) } table.map_column!(:worth) { |worth| "$#{worth}" } table end
We can define a number of common use methods under the
features/support
directory, for example creating acurrent_user
method and putting it underfeatures/support/current_user.rb
:def current_user # Code to mock a current user object end
We can also utilize Cucumber's
World
interface to mix in customized modules, for example, we can define anadd_headers
method:module CapybaraHeadersHelper def add_headers(headers) headers.each do |name, value| page.driver.browser.header(name, value) end end end World(CapybaraHeadersHelper)
This means that in our step definition we can invoke the
add_headers
method from theCapybaraHeadersHelper
module to add a customized HTTP header when requesting web pages during a test.
Methods can be reused, and so can steps! This is a widely used tip for writing good and DRY Cucumber steps, known as compound steps.
Considering a website provider's ability to log in with third-party accounts such as Facebook, Google, or OpenID, the feature can be described as follows:
Feature: Login with 3rd party account As a website user I can login with 3rd party account So that I don't have to register a new account Scenario: Login with Facebook account Given user landed at login page And he choose login with FacebookThen he should see the Facebook authorization window Scenario: Login with Google account Given user landed at login page And he choose login with Google Then he should see the Google authorization window Scenario: Login with OpenID account Given user landed at login page And he choose login with OpenID Then he should see the OpenID login window
The step
user choose login with **
can be implemented using the DRY principle as follows:Given /^he choose login with (.*)$/ do |account_provider| step %{user clicks on the #{account_provider} logo} step %{login with #{account_provider}} end Given /^user clicks on the (.*) logo$/ do |account_provider| # DOM operation to trigger clicking on the related logo end Given /^login with (.*)$/ do |account_provider| # Implement OAuth login per given 3rd party account end
In each step's definition, most of the time, the step starts with
^
and ends with$
. It looks as follows:Given /^user landed at login page$/ do end
Both
^
and$
are called "anchors". The preceding step uses^
and$
to match the stringuser landed at login page
exactly; then we could probably employ "unanchored steps" as follows:Then /^wait (\d+) seconds?/ do |seconds| sleep(seconds.to_i) end
Note at the end of the match we use a question mark,
?
, to match the flexible pluralization instead of$
, which means all steps containing "wait for x second(s)" will be matched by the preceding step definition, for example:When I wait 5 seconds after the certificate has been downloaded When I wait 4 seconds until the loading animation finished
Other than the preceding technical tips, the last one, and also the most important tip, is to keep your steps organized!
All the preceding tips are targeted at writing maintainable and DRY Cucumber steps. The last tip is to keep Cucumber steps organized, which is kind of a "soft" skill, even though it might be the most important! Categorizing features and step definition files, using tags or hooks, using Rake tasks to encapsulate common running features, and so on; these "rules" are unobtrusive but really important to keep the Cucumber tests maintainable and make daily BDD development life easier.