max pleaner max pleaner - 4 months ago 64
Ruby Question

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:


  • require my feature files

  • require my step files

  • and run some series of feature/scenario/steps.



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:


  • run a specific feature or step

  • continue to the rest of the script ( because
    execute!
    exits the program).



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:


  • It's necessary to separate out step name and argument. I would ideally like to be able to call something like
    step("foo bar")
    instead of writing
    steps["foo"].invoke(["bar"])
    . With my current attempt to invoke steps, the regex in the definitions is ignored.

  • It'd be nice if the CLI output formatters were used.



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:


  • defining regex matchers at the global scope

  • providing a string, finding the regex match, and executing the step

  • use formatters for output



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.

Answer

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
# => []
Comments