0

I am new to automating tests using Ruby (in combination with Selenium) and struggling to use the name of a constant instead of its value in my test_steps.rb.

I'm using the 3 files below:

LoginPage.rb:

class LoginPage < Base

  FIELDS = {
    'Username' => '//form[@id="login"]//input[@name="j_username"]',
  }

Base.rb:

def fill_field(field, value)
  find(:xpath, field, wait: 15).set(value)
  #logger("Filling field with xpath '" + field + "' with value '" +
             value + "'.") > this works fine but prints the xpath

  p "field: " + field
  log = field.scan(/\w+(?:'\w+)*/).last
  logger("Filling field '" + log + "' with value '" + value + "'.")

test_steps.rb:

And(/^logs in as user "([^"]*)"$/) do |userName|
  $login_name = search_excel('testdata/accounts.xlsx', userName, 1)
  @pg.fill_field(LoginPage::FIELDS['Username'], $login_name)

When executing the scenario I see this in my command prompt:

Given a user is on the oCRM login page # features/step_definitions/test_steps.rb:13
"field: //form[@id=\"login\"]//input[@name=\"j_username\"]"
28/12/2016 13:03:50 Filling field 'j_username' with value 'D0X02703'.

Note that the line in Base.rb prints the value for the constant matching "field"

'//form[@id="login"]//input[@name="j_username"]'

I would like it to print the constant itself: "LoginPage::FIELDS['Username']". The target is to print this in my logging:

"Filling field 'Username' with value 'DOX02703'."

I've tried printing "field.name", "field.value" and played around with local_variables, but no success yet. Help would be very welcome!

Also tried the other way around with definition below but ofcourse then it won't pick up the constant value and look for xpath "LoginPage::FIELDS['Username']":

  def fill_fields(page, type, field, value)
            find(:xpath, page + "::" + type + "['" + field + "']", wait: 15).set(value)
  • I think this might do it for you, assuming you exclude the value printing part! http://stackoverflow.com/q/2603617/6090706 – Kostas Andrianos Dec 28 '16 at 14:51
  • Thanks for your comment! I prefer not to use code I don't understand (or seems overcomplicated) so I won't be going with this. Maybe simply printing the xpath in the log will do :-) – Blapmeister Dec 29 '16 at 09:01
  • It's best not to use code you don't understand but, if you try to understand it you'll learn one or two things to use in the future – Kostas Andrianos Dec 29 '16 at 09:13

4 Answers4

1

Constants aren't objects in Ruby. Since you can only pass objects as arguments, and only call methods on objects (and objects are the only values, and method calls are the only way to do something), it is simply not possible to pass a constant as an argument or ask it for its name.

The only thing you can do is pass the name of the constant as an argument instead of or in addition to the object the constant points to.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • Constants most definitely are objects in Ruby, it's just that the user has already called the `#[]` method on the constant Hash and retrieved the value for the given key so there is no reference to the constant passed to the method – Thomas Walpole Dec 29 '16 at 00:54
  • Constants are *not* objects. E.g. see section 6.2.1 of the ISO Ruby Language Specification which clearly says (**bold** emphasis mine): "A variable is denoted by a name, and **refers to** an object, which is called the value of the variable. **A variable itself is not an object**." Or, see the book *The Ruby Programming Language* co-authored by matz himself (**bold** emphasis mine): "every **value** is an object". Note: it says every *value*, variables aren't values. Constants are kinds of variables. Why does `Module#constants` return an `Array` of `Symbol`s and not an `Array` of `Constants`? – Jörg W Mittag Dec 30 '16 at 01:22
  • Because it can't! It can't return an `Array` of `Constant`s, because constants aren't objects and arrays must contain objects. Why do `Module#const_get`, `Module#const_set`, `Module#const_defined?`, `Module#const_get`, `Module#deprecate_constant`, `Module#private_constant`, `Module#public_constant`, and `Module#remove_const`, take a `Symbol` representing the name of the constant as an argument instead of simply the constant directly? Because methods can only take objects as arguments and constants aren't objects. I don't know why this is so controversial. Constants and variables aren't objects – Jörg W Mittag Dec 30 '16 at 01:26
  • in *any* major mainstream language. There are only very few languages where variables and/or constants are objects, so I really don't understand why the statement that constants aren't objects in Ruby causes such confusion: that's how almost all languages work, that's what the spec says, that's what Ruby's creator says. – Jörg W Mittag Dec 30 '16 at 01:27
  • Yes - technically you are correct, the actual constant variable itself is not an object - the constant refers to an object, and yes the OPs question as written didn't fully make sense - however from what (s)he wanted the log to look like `"Filling field 'Username' with value 'DOX02703'."' it's pretty obvious they didn't want the actual "constant" they wanted the object the constant referred to available in the method - which was gone because they had already indexed into the hash. So yes -- technically you are correct the constant variable name is not an object - just what it refers to is. – Thomas Walpole Dec 30 '16 at 02:29
0

You could use a String with const_get :

class LoginPage # <Base
  FIELDS = {
    'Username' => '//form[@id="login"]//input[@name="j_username"]',
  }
end

def get_constant_from_string(param)
  puts "Parameter is #{param}"
  Object.const_get(param) if param.is_a?(String)
end

get_constant_from_string(LoginPage::FIELDS)
# Parameter is {"Username"=>"//form[@id=\"login\"]//input[@name=\"j_username\"]"}
# method returns nil

puts get_constant_from_string("LoginPage::FIELDS")['Username']
# Parameter is LoginPage::FIELDS
# //form[@id="login"]//input[@name="j_username"]
Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
  • I've experimented with this a little but can't get it to work. To set param I would have to concatenate values, leading to the issue where it will look for xpath "LoginPage::FIELDS['Username']" instead of the constant. My Ruby experience (overall programming experience too) is little so I could be misunderstanding. – Blapmeister Dec 29 '16 at 08:54
0

Essentially there is no difference between calling

@pg.fill_field(LoginPage::FIELDS['Username'], $login_name)

and

username = LoginPage::FIELDS['Username']
@pg.fill_field(username, $login_name)

it's just that the first one doesn't mention the temporary variable. Either way the only thing passed through to the method is the value associated with the 'Username' key in the hash. Looking at your class structure it seems like you have a number of 'Page' classes that inherit from base, and you're expecting fill_field to use the FIELDS constant from the class that inherited it. In that case you probably want

def fill_field(field, value)
  find(:xpath, self.class::FIELDS[field], wait: 15).set(value)

  logger("Filling field '" + field + "' with value '" + value + "'.")
end

and calling it as

@pg.fill_field('Username', $login_name)
Thomas Walpole
  • 48,548
  • 5
  • 64
  • 78
  • Looks great but I'm afraid this won't work for me. I got this message at first as we don't define constants in base.rb: `uninitialized constant Base::FIELDS (NameError)` After adding "FIELDS + {}" in base.rb I get `InvalidSelectorError: Unable to locate an element with the xpath expression because of the following error`. (as there is no constant 'Username' in base.rb) How is this supposed to know in what Page class to look for the constant? Would I need def fill_field in each Page object instead of in base.rb? – Blapmeister Dec 29 '16 at 08:52
  • @Blapmeister as long as you're calling fill_field on an object of the Page class which was derived from Base then `self.class::FIELDS` should refer to the FIELDS constant in the Page class since that is the actual class of the object, and all access to the constant will need to be of the form `self.class::FIELDS`. If you're calling it on a pure object of the Base class (not a class derived from Base) it won't work – Thomas Walpole Dec 29 '16 at 09:49
  • Thanks a lot Thomas! Got it to work this way, see setup below. The only things bothering me now is that I have to put def fill_field on each page object. Previously the page objects contained no classes, only constants. But as the actual functionality is still in base.rb I don't think this is an issue. – Blapmeister Dec 29 '16 at 10:33
0

Working solution based on Thomas Walpole's comment:

Base.rb:

def find_field(field)
    @fieldToFill = find(:xpath, self.class::FIELDS[field], wait: 15)
end

def set_value(value)
    @fieldToFill.set(value)
end

def fill_field_log(field, value)
    logger("Filling field '" + field + "' with value '" + value + "'.")           
end

LoginPage.rb:

def fill_field(field, value)
    find_field(field)
    set_value(value)
    fill_field_log(field, value)
end

test_steps.rb:

Given(/^a user is on the login page$/) do
    @pg = Base.new
    @lp = LoginPage.new
    @pg.visit_loginpage
end

And(/^logs in as user "([^"]*)"$/) do |userName|
    $login_name = search_excel('testdata/accounts.xlsx', userName, 1)
    login_password = search_excel('testdata/accounts.xlsx', userName, 2)

    @lp.fill_field('Username', $login_name)
    @lp.fill_field('Password', login_password)