How to create and use variables dynamically named by string values in Ruby?

800 Views Asked by At

I'm using SitePrism to create some POM tests. One of my page classes looks like this:

class HomePage < SitePrism::Page
    set_url '/index.html'
    element :red_colour_cell, "div[id='colour-cell-red']"
    element :green_colour_cell, "div[id='colour-cell-green']"
    element :blue_colour_cell, "div[id='colour-cell-blue']"

    def click_colour_cell(colour)
        case colour
            when 'red'
                has_red_colour_cell?
                red_colour_cell.click
            when 'green'
                has_green_colour_cell?
                green_colour_cell.click
            when 'blue'
                has_blue_colour_cell?
                blue_colour_cell.click
        end
    end
end

The method click_colour_cell() get its string value passed from a Capybara test step that calls this method. If I need to create additional similar methods in the future, it can become rather tedious and unwieldy having so many case switches to determine the code flow.

Is there some way I can create a variable that is dynamically named by the string value of another variable? For example, I would like to do something for click_colour_cell() that resembles the following:

    def click_colour_cell(colour)
        has_@colour_colour_cell?
        @colour_colour_cell.click
    end

where @colour represents the value of the passed value, colour and would be interpreted by Ruby:

    def click_colour_cell('blue')
        has_blue_colour_cell?
        blue_colour_cell.click
    end

Isn't this what instance variables are used for? I've tried the above proposal as a solution, but I receive the ambiguous error:

syntax error, unexpected end, expecting ':'
    end
    ^~~ (SyntaxError)

If it is an instance variable that I need to use, then I'm not sure I'm using it correctly. if it's something else I need to use, please advise.

2

There are 2 best solutions below

2
user11350468 On BEST ANSWER

Instance variables are used define properties of an object.

Instead you can achieve through the method send and string interpolation.

Try the below:

def click_colour_cell(colour)
  send("has_#{colour}_colour_cell?")
  send("#{colour}_colour_cell").click
end

About Send:

send is the method defined in the Object class (parent class for all the classes).

As the documentation says, it invokes the method identified by the given String or Symbol. You can also pass arguments to the methods you are trying to invoke.

On the below snippet, send will search for a method named testing and invokes it.

class SendTest
  def testing
    puts 'Hey there!'
  end
end


obj = SendTest.new
obj.send("testing")
obj.send(:testing)

OUTPUT

Hey there!
Hey there!

In your case, Consider the argument passed for colour is blue,

"has_#{colour}_colour_cell?" will return the string"has_blue_colour_cell?" and send will dynamically invoke the method named has_blue_colour_cell?. Same is the case for method blue_colour_cell

3
Kache On

Direct answer to your question

You can dynamically get/set instance vars with:

instance_variable_get("@build_string_as_you_see_fit")
instance_variable_set("@build_string_as_you_see_fit", value_for_ivar)

But...

A Warning!

I think dynamically creating variables here and/or using things like string-building method names to send are a bad idea that will greatly hinder future maintainability.

Think of it this way: any time you see method names like this:

click_blue_button
click_red_button
click_green_button

it's the same thing as doing:

add_one_to(1)   // instead of 1 + 1, i.e. 1.+(1)
add_two_to(1)   // instead of 1 + 2, i.e. 1.+(2)
add_three_to(1) // instead of 1 + 3, i.e. i.+(3)

Instead of passing a meaningful argument into a method, you've ended up hard-coding values into the method name! Continue this and eventually your whole codebase will have to deal with "values" that have been hard-coded into the names of methods.

A Better Way

Here's what you should do instead:

class HomePage < SitePrism::Page
  set_url '/index.html'

  elements :color_cells, "div[id^='colour-cell-']"

  def click_cell(color)
    cell = color_cells.find_by(id: "colour-cell-#{color}") # just an example, I don't know how to do element queries in site-prism
    cell.click
  end
end

Or if you must have them as individual elements:

class HomePage < SitePrism::Page
  set_url '/index.html'

  COLORS = %i[red green blue]

  COLORS.each do |color|
    element :"#{color}_colour_cell", "div[id='colour-cell-#{color}']"
  end

  def cell(color:)                 # every other usage should call this method instead
    @cells ||= COLORS.index_with do |color|
      send("#{color}_colour_cell") # do the dynamic `send` in only ONE place
    end
    @cells.fetch(color)
  end
end

home_page.cell(color: :red).click