I’ve been bashing my head against this for about three days now. I’ve created a class that models html pages and tells cucumber step definitions where to populate form data:
class FlightSearchPage
def initialize(browser, page, brand)
@browser = browser
@start_url = page
#Get reference to config file
config_file = File.join(File.dirname(__FILE__), '..', 'config', 'site_config.yml')
#Store hash of config values in local variable
config = YAML.load_file config_file
@brand = brand #brand is specified by the customer in the features file
#Define instance variables from the hash keys
config.each do |k,v|
instance_variable_set("@#{k}",v)
end
end
def method_missing(sym, *args, &block)
@browser.send sym, *args, &block
end
def page_title
#Returns contents of <title> tag in current page.
@browser.title
end
def visit
@browser.goto(@start_url)
end
def set_origin(origin)
self.text_field(@route[:attribute] => @route[:origin]).set origin
end
def set_destination(destination)
self.text_field(@route[:attribute] => @route[:destination]).set destination
end
def set_departure_date(outbound)
self.text_field(@route[:attribute] => @date[:outgoing_date]).set outbound
end
# [...snip]
end
As you can see, I’ve used instance_variable_set to create the variables that hold the references on the fly, and the variable names and values are supplied by the config file (which is designed to be editable by people who aren’t necessarily familiar with Ruby).
Unfortunately, this is a big, hairy class and I’m going to have to edit the source code every time I want to add a new field, which is obviously bad design so I’ve been trying to go a stage further and create the methods that set the variable names dynamically with define_method and this is what’s kept me awake until 4am for the last few nights.
This is what I’ve done:
require File.expand_path(File.dirname(__FILE__) + '/flight_search_page')
class SetFieldsByType < FlightSearchPage
def text_field(config_hash)
define_method(config_hash) do |data|
self.text_field(config_hash[:attribute] => config_hash[:origin]).set data
end
end
end
The idea is that all you need to do to add a new field is add a new entry to the YAML file and define_method will create the method to allow cucumber to populate it.
At the moment, I’m having problems with scope – Ruby thinks that define_method is a member of @browser. But what I want to know is: is this even feasible? Have I totally misunderstood define_method?
This is an appropriate case for metaprogramming, but it looks like you’re going about it the wrong way.
First of all, is there going to be a different config file for each instance of FlightSearchPage or just one config file that controls all pages? It looks like you’re loading the same config file regardless of the arguments to
initializeso I’m guessing your case is the former.If that is so, you need to move all of your metaprogramming code into the class (outside method definitions). I.e. when the class is defined, you want it to load the config file and then each instance is created based on that config. Right now you have it reloading the config file every time you create an instance, which seems incorrect. For example,
define_methodbelongs toModuleso it should appear in class scope, rather than in an instance method.On the other hand, if you do want a different config for each instance, you need to move all of your metaprogramming code into the singleton class e.g.
define_singleton_methodrather thandefine_method.