Description
The following method proposes an alternative approach to one of the solutions described in Jon M's answer.
Namely, instead of calling nested steps inside step definitions, such common blocks of steps can be extracted into external .feature
files which can be included
into your feature file (in a manner of speaking).
How-to
1. Expose utility / helper methods to be able to run steps parsed from a .feature
file
# features/support/env.rb
# expose Cucumber runtime
InstallPlugin do |_, registry|
runtime = registry.instance_variable_get('@registry').instance_variable_get('@runtime')
Cucumber.define_singleton_method(:runtime) { runtime }
end
# extend current World with methods to run dynamic (already parsed) steps
Before do
step_invoker = Cucumber::Runtime::SupportCode::StepInvoker.new(Cucumber.runtime.support_code)
define_singleton_method(:dynamic_steps) do |steps|
steps.each do |step|
dynamic_step(step)
end
end
define_singleton_method(:dynamic_step) do |step|
LOGGER.info("Running template step: #{step[:text]}")
step_invoker.step(step)
end
end
2. Create a template file which will contain the steps to be shared
# features/templates/my_profile.template.feature
@template
Feature: Steps to navigate to my_profile_page
Scenario: login_page
Given my app has started on "login_page"
And I enter "guest" in "user-field" on "login_page"
And I enter "1234" in "password-field" on "login_page"
And I press "login" on "login_page" and go to "welcome_page"
Scenario: welcome_page
Given that I am on "welcome_page"
And I click "my_profile_button" on "welcome_page" and go to "my_profile_page"
Scenario: my_profile_page
...
3. Create an utility module which will parse steps from a .feature
file
# features/support/template_parser.rb
require 'gherkin/parser'
require 'gherkin/pickles/compiler'
module TemplateParser
class << self
def read_from_template(template_path, from: nil, till: nil)
pickles = load_template(template_path)
flow = construct_flow(pickles)
slice_flow(flow, from, till)
end
private
def load_template(template_path)
source = {
uri: template_path,
data: File.read(template_path),
mediaType: 'text/x.cucumber.gherkin+plain'
}
def source.uri
self[:uri]
end
gherkin_document = Gherkin::Parser.new.parse(source[:data])
id_generator = Cucumber::Messages::IdGenerator::UUID.new
Gherkin::Pickles::Compiler.new(id_generator).compile(gherkin_document, source)
end
def construct_flow(pickles)
pickles.to_h do |pickle|
[
pickle.name,
pickle.steps.map(&:to_h).map { |step| step[:argument] ? step.merge(step[:argument]) : step }
]
end
end
def slice_flow(flow, from, till)
raise NameError, "From step '#{from}' does not exist!" unless from.nil? || flow.keys.include?(from)
raise NameError, "Till step '#{till}' does not exist!" unless till.nil? || flow.keys.include?(till)
from_idx = from.nil? ? 0 : flow.keys.index(from)
till_idx = till.nil? ? -1 : flow.keys.index(till)
flow.slice(*flow.keys[from_idx...till_idx])
end
end
end
4. Create a step definition that will load this template and inject the specified steps dynamically at runtime
And('I complete the {string} template from the {string} until the {string}') do |template, from, till|
template_path = "features/templates/#{template}.template.feature"
flow = TemplateParser.read_from_template(
template_path,
from: from.empty? ? nil : from,
till: till.empty? ? nil : till
)
flow.each_value { |steps| dynamic_steps(steps) }
end
5. Use this step inside your main feature file, by declaring which blocks of steps to use
# features/tests/welcome.feature
Feature: User is welcomed
Scenario: Verify that user sees welcome text
Given I complete the 'my_profile' template from the 'login_page' until the 'my_profile_page'
Then I see 'welcome' on 'welcome_page'
6. Make sure you omit the @template
.feature
files from being run in your tests
$ bundle exec cucumber --tags ~@template
Limitations
Con:
This method exposes some internals of the private API of cucumber-ruby
, which may change in future.
Con:
This is a non-standard way of sharing steps between feature files.
Helper methods are the preferred way to achieve this, as per FAQ.
Pro:
The common blocks of steps are syntax-highlighted, and have proper IntelliSense support in your editor of choice.
Pro:
You can encode entire "workflows" easily this way, allowing you to encode your workflow expectations in a DRY way.
Namely, you can reuse those workflow steps by completing the first part of a workflow, change a few things on a single page as per your test requirements, resume those workflow steps from the follow-up page, and add an appropriate verification at the end of the workflow that covers those test requirements.