简体   繁体   中英

Cucumber Ruby - A step within a module

Background

I'm writing both a cucumber library with a variety of different steps to use across multiple projects and attempting to reduce complexity of step definitions by splitting them into 3 different modules, for iOS, Android and Web - specifically projects that include all 3.

One of the steps libraries will include a load of security-based steps that I want to explicitly include into a project before using the steps.

The project split library will be explicitly included depending on what has been specified in the configuration:

if $profile[:app_type] == 'android'
   World(ProjectSteps::Android)
else 
   if $profile[:app_type] == 'ios'
      World(ProjectSteps::IOS)
   else
      World(ProjectSteps::Web)
   end
end

These are not to replace the helper methods, but to save us time when starting up on new projects and will also allow us to have project specific step definitions written in different ways depending on whether we are testing the web, native iOS app or native Android app built to have the exact same functionality, but are different enough to require a different step definition

The Issue

After defining the step within a module, the feature files can still execute this happily, even if the module has not been included in the "World" like this: World(CommonSteps::Security) , which is what you would usually use to let cucumber know about helper methods hidden away inside of modules.

When 'I provide my personal details' do
    select :title, 'Mr'
    fill :first_name, 'John'
    fill :last_name, 'Doe'

    unless $profile[:app_type] == 'web'
        click :progress
    end

    if $profile[:app_type] == 'android'
        fill :postcode, 'TE37ER'
        select :address, '1 Test Street'
        click :progress
        fill :occupation, 'Tester'
        fill :company, 'Test Company'
        click :progress
    else 
        fill :occupation, 'Tester'
        fill :company, 'Test Company'
        unless $profile[:app_type] == 'web'
           click :progress
        end
        fill :postcode, 'TE37ER'
        select :address, '1 Test Street'
        click :progress
    end
end

This step definition is trying to test 3 apps at once, but it is testing the exact same functionality, the exact same scenarios and the exact same features. If this was split into 3 step definitions, then it would be simpler to debug in the future, but the same feature file would be able to be used for each of them (which isn't out of the question, as there are many apps that share the exact same functionality across web and native mobile variants). In my opinion, this type of step definition is trying to achieve too much.

This would be easier to maintain despite being more, as it is simpler:

module ProjectSteps::IOS
   When 'I provide my personal details' do
       select :title, $user[:title]
       fill :first_name, $user[:first_name]
       fill :last_name, $user[:last_name]
       click :progress
       fill :occupation, $user[:occupation]
       fill :company, $user[:company]
       click :progress
       fill :postcode, $user[:postcode]
       select :address, $user[:line1]
       click :progress
   end
end
module ProjectSteps::Android
   When 'I provide my personal details' do
       select :title, $user[:title]
       fill :first_name, $user[:first_name]
       fill :last_name, $user[:last_name]
       click :progress
       fill :postcode, $user[:postcode]
       select :address, $user[:line1]
       click :progress
       fill :occupation, $user[:occupation]
       fill :company, $user[:company]
       click :progress
   end
end
module ProjectSteps::Web
   When 'I provide my personal details' do
       select :title, $user[:title]
       fill :first_name, $user[:first_name]
       fill :last_name, $user[:last_name]
       fill :occupation, $user[:occupation]
       fill :company, $user[:company]
       fill :postcode, $user[:postcode]
       select :address, $user[:line1]
       click :progress
   end
end
When 'some thing that is the same across platforms' do
   # Some stuff
end

Bear in mind, this is a simple version of what I'm trying to achieve, and doesn't show the full complexity of some of the issues that I'm trying to resolve. In this case, I would most likely go with the if / unless version rather than the split, but there are a few cases that are drastically more complex, and would benefit from being split into 3 sections.

We could also add silent checks for bugs that we found in development to make sure that these don't regress, and as the web, android and ios apps feature different bugs, we'd end up with a large amount of if / unless statements.

What have I tried? - I hear you ask

Well I'm either really close, or really far off.

Given , When and Then don't work as expected when within a different module, which is why I had to search for the method that I believe them to be an alias of.

Here is the resulting code:

require_relative 'xss.rb'
require 'cucumber'

module CommonSteps
  module Security
    Cucumber::RbSupport::RbDsl.register_rb_step_definition(
        'I attempt to write a step definition that has to be included to work', 
        Proc.new { 
            # Some stuff here
        })
  end
end

Registers the step definition absolutely fine. But that's part of the problem. I only want to register this step definition if the module has been included in the world of the project that I'm working on.

It would also mean that we can switch out the Web steps for the iOS and Android steps if we need to, while keeping the feature files the exact same. (Yes I know if statements are a thing, but too many, and step defs get complicated really fast.)

Edit

What I'm trying to achieve is not like the "Web steps" that we have seen in the past. No generic steps show code and are only in the language that we have agreed with the businesses that we work with, alongside the development team. As a lot of the projects we work on are cross platform, I'm in essence trying to achieve something that will switch which type of step definition is being used. - if you're using Chrome, use the Web version of this step definition, if you're using iOS, use the iOS version of this step definition, but also a means to include a variety of generic steps that are powered by text, which can link back to our page object model - keeping the scenarios completely business-based.

Given I am on the "Personal Details" page # (generic)
When I provide my personal details # (non-generic, but Web, iOS and Android versions exist)
But leave the "First Name" field blank # (generic)
And I attempt to continue to the next page # (generic)
Then I should see a validation error for the "First Name" text box stating: "Please provide your first name" # (generic)

For example, and if the validation is something that the business wants to know is working, and it'a part of the requirements that have been agreed with the business, is there a more business understandable way to communicate that information? - the why in this, is that we want to make sure the user fills in the information so that the validation does not show, but in the case that they don't provide that information, we should be also testing for the validation appearing in scenarios where it is needed.

We use the page object model in such a way that "Personal Details" will find the :personal_details key in the urls map. The key can be passed down to the next steps, linking to the Pages.personal_details method, which contains the :first_name key. All of our projects use this setup, and it's part of the documentation that goes with our core helper method library.

What I'm trying to achieve is not necessarily bad practice when used in the way that I'm suggesting, but could, if used incorrectly, be used as such.

There have been quite a few times in Cucumber's history when things like this have been done Cucumber itself used to have web steps, which was removed some time ago. These are now in a Gem https://github.com/cucumber/cucumber-rails-training-wheels . You might get some tips from that for your library.

That said, I would strongly recommend not writing a library of step definitions. Instead I would write a library of methods that can be used by step definitions. A crude example might help illustrate this.

Lets say you have a really complex method for logging in to an application. You could write a really commplicated step definition that deals with all sorts of logging in things such as

When I login (hugely complex regex to deal with things like ... # loads of code to deal with you params and regex's and make things work with lots of different scenarios

or you could write a method like

def login(as:, args={})

and let people use this method when they write stuff eg

When 'I login' do
  login as: @i
end

When 'I login as Fred' do
  login as: create_or_find_user(firstname: 'Fred')
end

or

When 'I login as Fred with Jill's password' do
  login as: @fred, password: @jill.password
end

The helper methods provide utility to help you write simple step definitions that are appropriate to your individual context. A shared step definition restricts you to using something that is highly complex and cannot have anything context specific.

Scenarios should be context specific and allow flexible simple language that is specific to the context of the individual World they are part of. They should be all about Why something is being done and What that thing is, and have nothing about How something is being done. By definition they do not share and so by definition neither to step definitions.

Once you've left a step definition by making a call you are into the realm of code, and code is really effective at sharing

Cucumber has learnt the lesson that shared step definitions are a really bad idea (see http://aslakhellesoy.com/post/11055981222/the-training-wheels-came-off ). Be wary of repeating past mistakes.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM