22

I'm new to cucumber, but enjoying it.

I'm currently writing some Frank tests, and would like to reuse blocks of cucumber script across multiple features - I'd like to do this a the cucumber level if possible (not inside the ruby).

For example, I might have 4 scripts that all start by doing the same login steps:

  given my app has started
     then enter "guest" in "user-field"
     and enter "1234" in "password-field"
     and press "login"
  then I will see "welcome"
  then *** here's the work specific to each script ***

Is there any way to share these first 5 lines across multiple scripts? Some kind of "include" syntax?

Stuart
  • 66,722
  • 7
  • 114
  • 165

3 Answers3

25

Generally there are 2 approaches:

Backgrounds

If you want a set of steps to run before each of the scenarios in a feature file:

Background:
     given my app has started
     then enter "guest" in "user-field"
     and enter "1234" in "password-field"
     and press "login"
     then I will see "welcome"

Scenario: Some scenario
    then *** here's the work specific to this scenario ***

Scenario: Some other scenario
    then *** here's the work specific to this scenario ***

Calling steps from step definitions

If you need the 'block' of steps to be used in different feature files, or a Background section is not suitable because some scenarios don't need it, then create a high-level step definition which calls the other ones:

Given /^I have logged in$/ do
    steps %Q {
         given my app has started
         then enter "guest" in "user-field"
         and enter "1234" in "password-field"
         and press "login"
         then I will see "welcome"
    }
end

Also, in this case I'd be tempted not to implement your common steps as separate steps at all, but to create a single step definition: (assuming Capybara)

Given /^I have logged in$/ do
    fill_in 'user-field', :with => 'guest'
    fill_in 'password-field', :with => '1234'
    click_button 'login'
end

This lends a little bit more meaning to your step definitions, rather than creating a sequence of page interactions which need to be mentally parsed before you realise 'oh, this section is logging me in'.

Jon M
  • 11,669
  • 3
  • 41
  • 47
  • +1 Interesting - thanks - will read through those wiki pages. – Stuart Oct 04 '11 at 13:19
  • OK - ticked as answered - thanks - I think I'm just going to reorg the way I build my scenarios - multiple to one file looks OK. Can I just check that there's definitely no pre-built way of sharing "Background" blocks between files. (I do get the funniest results when I search for answers about "cucumber"!) – Stuart Oct 04 '11 at 13:27
  • If there was, you might have better luck searching under 'Gherkin' as that's technically the language your feature files are written in. I'm fairly sure there's no 'include' syntax of any sort though, you can even [see the grammar](https://github.com/cucumber/gherkin/blob/master/ragel/lexer_common.rl.erb) and check for yourself! – Jon M Oct 04 '11 at 16:11
  • You said Backgrounds run before _all_ scenarios in a feature file. The docs you linked to say they run before _each_ scenario. – Ovesh Mar 12 '13 at 02:20
  • I did mean 'each', just worded it poorly, good catch - thanks! – Jon M Mar 12 '13 at 15:47
0

A better approach is suggested to use ruby level "methods" to code reuse instead of nested steps from code maintenance and debugging perspective.

Here is the link to more detail: Reuse Cucumber steps

Community
  • 1
  • 1
Vishal Aggarwal
  • 1,929
  • 1
  • 13
  • 23
0

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.

mucaho
  • 2,119
  • 20
  • 35