Ruby instance variables within class? - ruby

So I was looking over page objects for Ruby testing here:
http://elementalselenium.com/tips/7-use-a-page-object
And down on the page within this bit of code:
class GoogleSearch
SEARCH_BOX = { id: 'gbqfq' }
SEARCH_BOX_SUBMIT = { id: 'gbqfb' }
TOP_SEARCH_RESULT = { css: '#rso .g' }
attr_reader :driver
def initialize(driver)
#driver = driver
visit
verify_page
end
def visit
driver.get ENV['base_url']
end
def search_for(search_term)
driver.find_element(SEARCH_BOX).clear
driver.find_element(SEARCH_BOX).send_keys search_term
driver.find_element(SEARCH_BOX_SUBMIT).click
end
def search_result_present?(search_result)
wait_for { displayed?(TOP_SEARCH_RESULT) }
driver.find_element(TOP_SEARCH_RESULT).text.include? search_result
end
private
def verify_page
driver.title.include?('Google').should == true
end
def wait_for(seconds=5)
Selenium::WebDriver::Wait.new(:timeout => seconds).until { yield }
end
def displayed?(locator)
driver.find_element(locator).displayed?
true
rescue Selenium::WebDriver::Error::NoSuchElementError
false
end
end
It defines #driver = driver, but then in all the other methods it uses just driver. Shouldn't all the other methods be using the initialized variable #driver? Or is this just simply a case of mis-documentation?
I mean after all what was the point of declaring #driver if we never use it again?

attr_reader :driver is just a helper (macro) that defines following method:
def driver
#driver
end
While attr_writer :driver would define the following:
def driver=(value)
#driver = value
end
and attr_accessor :driver would define both, a reader and writer method.
So whenever you see driver it calls the method that accesses #driver. It is good practice to access instance variable via a getter. Makes the code easier to refactor.
Assuming you want to raise an exception if driver is unset, you can remove the attr_reader and add your own definition of driver without changing the rest of the code:
def driver
raise "no driver set" if !#driver
#driver
end

attr_reader :driver means that #driver is accessible as driver. When the code refers to driver, it is getting #driver.
You can use #driver within the class if you prefer, with or without the attr_reader declaration.

There is this line attr_reader :driver at the top of the class which is a macro for:
def driver
#driver
end
The advantage to prefer this getter methods over direct #driver calls is that it makes future refactorings easier.

Related

Ruby TestCase: Define instance variable in `self.startup` method

I have this Ruby code:
class GoogleTestCase < BaseTestCase
def test_search
#browser.find_element(:name, 'q').send_keys "Hello Ruby"
#browser.find_element(:name, 'btnK')
end
end
And then I run the GoogleTestCase through this file:
...
class BaseTestCase < Test::Unit::TestCase
def self.startup
#browser = Selenium::WebDriver.for :chrome
#browser.get('https://google.com')
end
def self.shutdown
#browser.quit
end
end
exit Test::Unit::AutoRunner.run(true, test_dir)
After launching, everything is fine. Selenium will run Chrome browser, it opens Google web page. But when the test_search method is fired, Ruby can't see #browser variable:
How can I define #browser variable in self.startup method so inside the test_search method I can see it?
The error is because you try to access instance variable #browser, which is defined on class level. Because startup and shutdown are class methods, #browser is class variable accordingly.
You can use ##browser to access class variables from instance level.
class GoogleTestCase < BaseTestCase
def test_search
##browser.find_element(:name, 'q').send_keys "Hello Ruby"
##browser.find_element(:name, 'btnK')
end
end
Keep in mind, that ##browser is the same across all instances of such class.
Also, you can encapsulate the way you access the browser variable in helper method:
class BaseTestCase < Test::Unit::TestCase
def self.startup
#browser = Selenium::WebDriver.for :chrome
#browser.get('https://google.com')
end
def self.shutdown
#browser.quit
end
def browser
##browser
end
end
class GoogleTestCase < BaseTestCase
def test_search
browser.find_element(:name, 'q').send_keys "Hello Ruby"
browser.find_element(:name, 'btnK')
end
end

Ruby configuration techniques

I am very new to ruby and I just spent time studying patterns from the existing ruby projects in github. Now, I landed on the twitter's ruby project and noticed these lines in their configuration:
client = Twitter::REST::Client.new do |config|
config.consumer_key = "YOUR_CONSUMER_KEY"
config.consumer_secret = "YOUR_CONSUMER_SECRET"
config.access_token = "YOUR_ACCESS_TOKEN"
config.access_token_secret = "YOUR_ACCESS_SECRET"
end
In the declaration of this method call, I also noticed this:
module Twitter
class Client
include Twitter::Utils
attr_accessor :access_token, :access_token_secret, :consumer_key, :consumer_secret, :proxy
def initialize(options = {})
options.each do |key, value|
instance_variable_set("##{key}", value)
end
yield(self) if block_given?
end
...
Now, as I do my practice, I copied the same logic but observe the content of "initialize" method.
module Main
class Sample
attr_accessor :hello, :foo
def initialize(options={})
yield(self) if block_given?
end
def test
#hello
end
end
end
And call it (same on how twitter code above does)
sample = Main::Sample.new do |config|
config.hello = "world"
config.foo = "bar"
end
puts "#{sample.hello} #{sample.foo}" # outputs => world bar
puts sample.test # outputs => world
Now, my question is that even though I don't have these lines in my code (see the code block from twitter above) inside my "initialize" method,
options.each do |key, value|
instance_variable_set("##{key}", value)
end
the code
puts "#{sample.hello} #{sample.foo}" and puts sample.test still works fine. Why is this so? How was the instance variable really set here?
It's because you're manually calling them with thing like config.hello= and config.foo=.
What won't work without that chunk of code is this:
Main::Sample.new(hello: 'world')
You'll need that part to pick up the options and apply them.
That Twitter version is pretty slack. Normally you'd want to test that there's a property with that name instead of just randomly assigning instance variables. Typically this is done with a white-list of some sort:
ATTRIBUTES = %i[ hello world ]
attr_accessor *ATTRIBUTES
def initialize(options = nil)
options and options.each do |attr, value|
if (ATTRIBUTES.include?(attr))
send("#{attr}=", value)
else
raise "Unknown attribute #{attr.inspect}"
end
end
yield self if (block_given?)
end
That will raise exceptions if you call with invalid options.

Dynamically create a class inherited from ActiveRecord?

I want to be able to dynamically create classes, for scripting outside my Rails app, that inherit from ActiveRecord.
I'm stuck on something like this:
require 'active_record'
def create_arec(table_name)
Class.new ActiveRecord::Base do
self.table_name = table_name
yield
end
end
Band = create_arec 'bands' do
scope :only_rock, -> {where genre: 'rock'}
end
rock_bands = Band.only_rock #undefined method `only_rock'
How do I make it work, or can someone show me better way to do it?
Nailed it:
def create_arec(table_name, &block)
klass = Class.new(ActiveRecord::Base){self.table_name = table_name}
klass.class_eval &block
klass
end
thanks #phoet

Setting up configuration settings when writing a gem

I'm writing a gem which I would like to work with and without the Rails environment.
I have a Configuration class to allow configuration of the gem:
module NameChecker
class Configuration
attr_accessor :api_key, :log_level
def initialize
self.api_key = nil
self.log_level = 'info'
end
end
class << self
attr_accessor :configuration
end
def self.configure
self.configuration ||= Configuration.new
yield(configuration) if block_given?
end
end
This can now be used like so:
NameChecker.configure do |config|
config.api_key = 'dfskljkf'
end
However, I don't seem to be able to access my configuration variables from withing the other classes in my gem. For example, when I configure the gem in my spec_helper.rb like so:
# spec/spec_helper.rb
require "name_checker"
NameChecker.configure do |config|
config.api_key = 'dfskljkf'
end
and reference the configuration from my code:
# lib/name_checker/net_checker.rb
module NameChecker
class NetChecker
p NameChecker.configuration.api_key
end
end
I get an undefined method error:
`<class:NetChecker>': undefined method `api_key' for nil:NilClass (NoMethodError)
What is wrong with my code?
Try refactoring to:
def self.configuration
#configuration ||= Configuration.new
end
def self.configure
yield(configuration) if block_given?
end
The main issue is that you've applied too much indirection. Why don't you just do
module NameChecker
class << self
attr_accessor :api_key, :log_level
end
end
and be done with it? You could also override the two generated readers right afterwards so that they ensure the presence of the environment that you need...
module NameChecker
class << self
attr_accessor :api_key, :log_level
def api_key
raise "NameChecker really needs is't api_key set to work" unless #api_key
#api_key
end
DEFAULT_LOG_LEVEL = 'info'
def log_level
#log_level || DEFAULT_LOG_LEVEL
end
end
end
Now, the actual (technical) problem is that you are defining a class called NetChecker and while defining it you are trying to print the return value of the api_key call on an assumed Configuration object (so you are violating the law of Demeter here). This fails, because you are defining NetChecker before anyone really has had time to define any configuration. So you are in fact requesting api_key before the configure method has been called on NameChecker, so it has nil in it's configuration ivar.
My advice would be to remove the overengineering and try again ;-)

How can one set property values when initializing an object in Ruby?

Given the following class:
class Test
attr_accessor :name
end
When I create the object, I want to do the following:
t = Test.new {name = 'Some Test Object'}
At the moment, it results in the name attribute still being nil.
Is that possible without adding an initializer?
ok,
I came up with a solution. It uses the initialize method but on the other hand do exactly what you want.
class Test
attr_accessor :name
def initialize(init)
init.each_pair do |key, val|
instance_variable_set('#' + key.to_s, val)
end
end
def display
puts #name
end
end
t = Test.new :name => 'hello'
t.display
happy ? :)
Alternative solution using inheritance. Note, with this solution, you don't need to explicitly declare the attr_accessor!
class CSharpStyle
def initialize(init)
init.each_pair do |key, val|
instance_variable_set('#' + key.to_s, val)
instance_eval "class << self; attr_accessor :#{key.to_s}; end"
end
end
end
class Test < CSharpStyle
def initialize(arg1, arg2, *init)
super(init.last)
end
end
t = Test.new 'a val 1', 'a val 2', {:left => 'gauche', :right => 'droite'}
puts "#{t.left} <=> #{t.right}"
As mentioned by others, the easiest way to do this would be to define an initialize method. If you don't want to do that, you could make your class inherit from Struct.
class Test < Struct.new(:name)
end
So now:
>> t = Test.new("Some Test Object")
=> #<struct Test name="Some Test Object">
>> t.name
=> "Some Test Object"
There is a general way of doing complex object initialization by
passing a block with necessary actions. This block is evaluated in the
context of the object to be initialized, so you have an easy access to
all instance variables and methods.
Continuing your example, we can define this generic initializer:
class Test
attr_accessor :name
def initialize(&block)
instance_eval(&block)
end
end
and then pass it the appropriate code block:
t = Test.new { #name = 'name' }
or
t = Test.new do
self.name = 'name'
# Any other initialization code, if needed.
end
Note that this approach does not require adding much complexity
to the initialize method, per se.
As previously mentioned, the sensible way to do this is either with a Struct or by defining an Test#initialize method. This is exactly what structs and constructors are for. Using an options hash corresponding to attributes is the closest equivalent of your C# example, and it's a normal-looking Ruby convention:
t = Test.new({:name => "something"})
t = Test.new(name: "something") # json-style or kwargs
But in your example you are doing something that looks more like variable assignment using = so let's try using a block instead of a hash. (You're also using Name which would be a constant in Ruby, we'll change that.)
t = Test.new { #name = "something" }
Cool, now let's make that actually work:
class BlockInit
def self.new(&block)
super.tap { |obj| obj.instance_eval &block }
end
end
class Test < BlockInit
attr_accessor :name
end
t = Test.new { #name = "something" }
# => #<Test:0x007f90d38bacc0 #name="something">
t.name
# => "something"
We've created a class with a constructor that accepts a block argument, which is executed within the newly-instantiated object.
Because you said you wanted to avoid using initialize, I'm instead overriding new and calling super to get the default behavior from Object#new. Normally we would define initialize instead, this approach isn't recommended except in meeting the specific request in your question.
When we pass a block into a subclass of BlockInit we can do more than just set variable... we're essentially just injecting code into the initialize method (which we're avoiding writing). If you also wanted an initialize method that does other stuff (as you mentioned in comments) you could add it to Test and not even have to call super (since our changes aren't in BlockInit#initialize, rather BlockInit.new)
Hope that's a creative solution to a very specific and intriguing request.
The code you're indicating is passing parameters into the initialize function. You will most definitely have to either use initialize, or use a more boring syntax:
test = Test.new
test.name = 'Some test object'
Would need to subclass Test (here shown with own method and initializer) e.g.:
class Test
attr_accessor :name, :some_var
def initialize some_var
#some_var = some_var
end
def some_function
"#{some_var} calculation by #{name}"
end
end
class SubClassedTest < Test
def initialize some_var, attrbs
attrbs.each_pair do |k,v|
instance_variable_set('#' + k.to_s, v)
end
super(some_var)
end
end
tester = SubClassedTest.new "some", name: "james"
puts tester.some_function
outputs: some calculation by james
You could do this.
class Test
def not_called_initialize(but_act_like_one)
but_act_like_one.each_pair do |variable,value|
instance_variable_set('#' + variable.to_s, value)
class << self
self
end.class_eval do
attr_accessor variable
end
end
end
end
(t = Test.new).not_called_initialize :name => "Ashish", :age => 33
puts t.name #=> Ashish
puts t.age #=> 33
One advantage is that you don't even have to define your instance variables upfront using attr_accessor. You could pass all the instance variables you need through not_called_initialize method and let it create them besides defining the getters and setters.
If you don't want to override initialize then you'll have to move up the chain and override new. Here's an example:
class Foo
attr_accessor :bar, :baz
def self.new(*args, &block)
allocate.tap do |instance|
if args.last.is_a?(Hash)
args.last.each_pair do |k,v|
instance.send "#{k}=", v
end
else
instance.send :initialize, *args
end
end
end
def initialize(*args)
puts "initialize called with #{args}"
end
end
If the last thing you pass in is a Hash it will bypass initialize and call the setters immediately. If you pass anything else in it will call initialize with those arguments.

Resources