max pleaner
max pleaner

Reputation: 26758

How can I define and call cucumber steps/scenarios outside of the cucumber cli?

I have a working cucumber setup. My features/ folder has feature and step definitions, and I can call cucumber at the command line to run the test suite.

I also have a separate script which I would like to enable to call some of the cucumber tests. I know that in a cucumber step definition, it's possible to call a step from another step. But I don't know a way to:

From the relishApp cucumber api docs, I've gathered this:

require 'cucumber'

# run all features
runtime = Cucumber::Runtime.new
Cucumber::Cli::Main.new([]).execute!(runtime)

This will run all my cucumber tests in the exact same way as if I has run cucumber from the command line, formatting included. However, I'm not sure how use this approach to:

I've also been looking at the cucumber source code to try and write/invoke steps dynamically:

require 'cucumber'

Given(/foo/) {}
# this raises an error:
# NoMethodError: undefined method `register_rb_step_definition'
# for Cucumber::RbSupport::RbLanguage:Class
# from /usr/local/lib/ruby/gems/2.0.0/gems/cucumber/2.4.0/lib
#       /cucumber/rb_support/rb_dsl.rb:28
# :in `register_rb_step_definition

# A way to make `'register_rb_step_definition'` succeed:
runtime = Cucumber::Runtime.new
config = Cucumber::Configuration.new
language = Cucumber::RbSupport::RbLanguage.new(runtime, config)
dsl = Cucumber::RbSupport::RbDsl
dsl.rb_language = language
steps = {}
steps["foo"] = dsl.register_rb_step_definiton(/foo (.+)/, ->(args) { puts args })

# Now I have a 'steps' hash like {"foo" => foo_step_object}
# I can call a step like so:
steps["foo"].invoke(["bar"]) # the argument to invoke is an array or hash
# this will print bar to the console like the step proc instructed

This defines and invokes tests successfully, but there are a few shortcomings:

I've been looking at some discussion about Cucumber and it seems there's a push to remove the capability to call a step using the steps method. In other words, all step nesting should be done through refactoring the code into separate methods, not by referencing other steps. I understand the inspiration for this notion, but I still envision a use-case for:

This is basically how Cucumber works already if it's run with the cucumber shell command, although it requires all steps to be run within a feature & scenario. If my application only needs 'steps', but needs to define a global 'feature' and 'scenario', so be it, but I'd still like to use Ruby only and not divert to the cucumber shell command.

Upvotes: 1

Views: 1792

Answers (2)

moertel
moertel

Reputation: 1559

This is an example of what works for me, defining and invoking steps manually:

require 'cucumber' # must require everything; otherwise Configuration cannot be initialized

config = Cucumber::Configuration.new
dsl = Object.new.extend(Cucumber::RbSupport::RbDsl)
rb_language = Cucumber::RbSupport::RbLanguage.new(:unused, config)
step_search = Cucumber::StepMatchSearch.new(rb_language.method(:step_matches), config)

dsl.Given(/hello (.*)/) { |foo| puts "argument given was: #{foo}" }

match = step_search.call('hello world').first
match.step_definition.invoke(match.args) # => argument given was: world

It will raise exceptions for redundant or ambiguous step definitions, too:

dsl.Given(/hello (.*)/) {  }
dsl.Given(/(.*) world/) {  }
step_search.call('hello world')
# => Cucumber::Ambiguous: Ambiguous match of "hello world"

If you intend to also have actual feature files or step definition files around but don't want them to be included, then have in mind that initialising Configuration without any parameters will autoload some folders:

config = Cucumber::Configuration.new
config.autoload_code_paths
# => ["features/support", "features/step_definitions"]

config = Cucumber::Configuration.new(autoload_code_paths: [])
config.autoload_code_paths
# => []

Upvotes: 2

Sebastian Lenartowicz
Sebastian Lenartowicz

Reputation: 4864

What you want is

require 'cucumber'
require 'path_to_step_defs.rb'

What this does is load the file path_to_step_defs.rb once, and only once. Then you don't need the load_step_definitions method, because the script file has already loaded what it needs.

Upvotes: 0

Related Questions