cucumber / cucumber-ruby

Cucumber for Ruby. It's amazing!
https://cucumber.io
MIT License
5.17k stars 1.12k forks source link

Cucumber "Transform" replacement, ParameterType, does not work for all use-cases, especially Tables #1551

Closed jim-edwards closed 1 year ago

jim-edwards commented 3 years ago

We have a Transform that loops through table entries and executes functions if the column itself contains some specific data (IE, its based on the result, not the regex). I am struggling to see how this could be done with ParameterType, doesn't seem possible.. Here is a sample of the code for reference:

Transform /table:.*/ do |table|
  table.map_headers! { |header| header.downcase.to_sym }
  table.hashes.each do |row|
    row.each do |column, value|
      if row[column] =~ /_CONFIG_/
        row[column]=TestSys::Easy.current_session.helper.add_config_settings(row[column])
      end
      if row[column] =~ /_UNIQ_/
        row[column]=TestSys::Easy.current_session.helper.replace_uniq_strings(row[column])
      end
      if row[column] =~ /_LOCAL_/
        row[column]=TestSys::Easy.current_session.helper.add_local_settings(row[column])
      end
      if row[column] =~ /_FEATURE_/
        row[column]=TestSys::Easy.current_session.helper.add_feature_settings(row[column])
      end
      if row[column] =~ /_RANDOMHEX_/
        row[column]=TestSys::Easy.current_session.helper.add_hex_string(row[column])
      end
      if row[column] =~ /_RANDOMALPHANUMERIC_/
        row[column]=TestSys::Easy.current_session.helper.process_random_number(row[column])
      end
      if row[column].include? '\s'
        row[column] = row[column].gsub('\s', ' ')
      end
    end
  end
  table
end

The above allows the following values to be replaced in steps:

And I assign the LOCAL variable name "TEST" the value "auto_RANDOMALPHANUMERIC_5"

The above transform will do a match, find the RANDOMALPHANUMERIC because its set the value and call the function above, replacing it with the return value from the function.

It also works in a table as you show, for example:

Then I fill in the fields below:
  | id            | value                |
  | personalEmail | noreply@test.com     |
  | @Field1       | _RANDOM_15           |

Both of those appear to work. Note that neither use the "Example" framework of a test case to execute the same Scenario multiple times.

mattwynne commented 3 years ago

Thanks for writing this up Jim. I suspect that the Transform call here is not providing the replacement for the step And I assign the LOCAL variable name "TEST" the value "auto_RANDOMALPHANUMERIC_5" but only for the data table values. I expect you have another step def somewhere that's also calling TestSys::Easy.current_session.helper.process_random_number

mattwynne commented 3 years ago

You're right that parameter types only work for regular steps, not for data tables. I think your workaround for now would be to refactor to extract a helper method out of your Transform function:

Transform /table:.*/ do |table|
  expand_placeholders_in table
end

module Helpers
  def expand_placeholders_in(table)
    table.map_headers! { |header| header.downcase.to_sym }
    table.hashes.each do |row|
      row.each do |column, value|
        if row[column] =~ /_CONFIG_/
          row[column]=TestSys::Easy.current_session.helper.add_config_settings(row[column])
        end
        if row[column] =~ /_UNIQ_/
          row[column]=TestSys::Easy.current_session.helper.replace_uniq_strings(row[column])
        end
        if row[column] =~ /_LOCAL_/
          row[column]=TestSys::Easy.current_session.helper.add_local_settings(row[column])
        end
        if row[column] =~ /_FEATURE_/
          row[column]=TestSys::Easy.current_session.helper.add_feature_settings(row[column])
        end
        if row[column] =~ /_RANDOMHEX_/
          row[column]=TestSys::Easy.current_session.helper.add_hex_string(row[column])
        end
        if row[column] =~ /_RANDOMALPHANUMERIC_/
          row[column]=TestSys::Easy.current_session.helper.process_random_number(row[column])
        end
        if row[column].include? '\s'
          row[column] = row[column].gsub('\s', ' ')
        end
      end
    end
  table
end

World(Helpers)

Then, you can use that helper function to manually expand the placeholders everywhere you take a table as a parameter:

Given /a step that takes a table:/ do |table|
  expand_placeholders_in table
  ... # do stuff with the expanded table data
end

That would at least allow you to get rid of the dependency on Transform and upgrade to a more modern version of Cucumber while we figure out how to implement this functionality again.

luke-hill commented 3 years ago

So in essence the issue here is that we cannot use the cukeexp parameter type because the capture is done anonymously (i.e. a {} is not provided because it's a data table).

Instead of trying to make a fit b, why don't we come at this from a different way @mattwynne

Why don't we make the table "methods" and manipulability bits function outside, maybe through including a helper or setting a config option? Although they already are by default available in the cucumber world, so actually this point is redundant!

Also whilst I haven't tried it. In theory could you not capture the data table using a regex that included new lines (I appreciate that's not pretty)

mattwynne commented 3 years ago

@luke-hill yes, I think that's the essence of the problem.

I was just suggesting a workaround for the OP, given the way things are.

I'm sure we could add something, either as a standalone helper, or something in the DSL (e.g. TransformDataTables) or as methods on the DataTable instances, to help people do this. We need to understand a bit more about the use-cases, I think.

jim-edwards commented 3 years ago

The primary use-case we are using it for is to allow variables to be set in the feature itself, which is replaced by the code to an actual value. For example:

Replace the string "auto_RANDOMALPHANUMERIC_5" with something like "auto_ABC4321_5". Or some other variable, doesn't have to just be random data.

mpkorstanje commented 3 years ago

@mattwynne The general case, turning a table into any other arbitrary object is useful. If an object is used in multiple step definitions it avoids the need to do so in each step.

The common/datatable module has several examples.

@luke-hill

Because ruby lacks types you'll need a way in the step definition to select which table transform should be used. Something like:

Given /a step that takes a table:/, expand_place_holders do |table|
  ... # do stuff with the expanded table data
end

Dunno if that works for ruby. In this case expand_place_holders is a function that processes the table and has access to the world context.

mattwynne commented 3 years ago

So @jim-edwards you'd like a way to be able to map over the cells in the table and transform the values in some of them, ideally before they're passed to the step definition?

jim-edwards commented 2 years ago

Sorry, missed the notification on this response. Yes, the idea is the variables can be "replaced", basically a search and replace. Ideally before the step is run, otherwise every single step must be modified to do the variable replacement (1,000s in our case). This is really useful for creating variables and reference them by name, so there can be for example, things loaded from the config file, command-line, or random values generated or even save/recall simple variables. All are possible with the previous system without having to specifically code the step to accept the variable itself.

luke-hill commented 2 years ago

Yeh i think adding these as module helpers and exposing them on the DataTable construct will be the easiest way forwards. Just a case of when/if we prioritise this.

luke-hill commented 2 years ago

Looking back at this, I've seen we have some methods in place already for altering the keys. symbolise_hashes takes the input table and converts all the keys to downcased snakecased keys. I would therefore advocate either something locally in your repo or merged into the main whereby you extend the table cell class.

luke-hill commented 1 year ago

Closing this as two solutions have been specified that would be better solutions.