One of the problems you face when writing a metaframework is getting configuration information to the various appendages that it grows. In my case, I have got Selenium (Selenese), and Selenium RC (Python, Java, Ruby) to deal with. While this might seem easy since they are all Selenium based, their structure is quite different. Yesterday afternoon I stopped working around the problem and managed to get all my tests reading information from the same file.

Like most programming activities, my framework grew pretty organically. I knew eventually I would want some sort of config file when I started and when I needed it I just plunked it into the main script. As the processing got larger I encapsulated some stuff in function and eventually into it’s own module. (The framework is in Jython BTW) Yesterday those modules get wrapped in a class and turned into a Singleton.

In Python there appears to be two ways of more or less solving the single instance problem; the Singleton Pattern and the Borg Pattern. I initially played with the Borg one because it has a great name, but decided it was not the proper solution because as once the __new__() method completes it would execute the __init__ which is not something I wanted to do. (At the time I was overriding __init__ to read my config file, etc. but I’ve re-engineering things slightly so I think I could now use this pattern. Oh well, not going to change working code now.)

So here is my config file class which is stored in the module fmwrk_config. I mention the module here explicitly because I initially had it as config, but that clashed with a log4j class.

<pre lang="python">import xml.dom

class config(object):
    # singleton
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(config, cls).__new__(cls, *args, **kwargs)
        return cls._instance
    
    def configure(self, config_file):
        # parse config
        config_dom = xml.dom.minidom.parse(config_file)

        # environment
        self.environment = {}
        environment_nodes = config_dom.getElementsByTagName("environment")[0]
        for n in environment_nodes.childNodes:
            if n.nodeType == xml.dom.Node.ELEMENT_NODE:
                self.environment[n.tagName] = n.firstChild.data

        #users
        self.users = {}
        user_nodes = config_dom.getElementsByTagName("user")
        for user_node in user_nodes:
            for el in user_node.childNodes:
                if el.nodeType == xml.dom.Node.ELEMENT_NODE:
                    if user_node.getAttribute("id") not in self.users:
                        self.users[user_node.getAttribute("id")] = {}
                    self.users[user_node.getAttribute("id")][el.tagName] = el.firstChild.data

Inside the main framework, I read in the config file path from the argument list and create the file

<pre lang="python"># argument handling
try:
    opts, args = getopt.getopt(sys.argv[1:], "c:", ["config="])
except:
    usage()
    sys.exit(1)
# make sure we have our config file
config_file = ""
for o, a in opts:
    if o in ("-c", "--config"):
        config_file = a
if config_file == "":
    usage()
    sys.exit(1)
else:
    if not os.path.exists(os.path.abspath(config_file)):
        print "config file (%s) does not exist" % os.path.abspath(config_file)
        sys.exit(1)

cf = fmwrk_config.config()
cf.configure(config_file)

All tests are currently launched from within the main script (a later refactoring will make script ‘types’ pluggable) with the Selenese ones building a test_suite.html dynamically based upon a regex directory crawl. Python based Selenium RC scripts are done through the standard unittest module where a suite object is populated in a similar way as the Selenese tests.

This is what initially caused this problem since the Selenese tests used the config file for runtime information (like usernames and credentials) but the Selenium RC ones could not get access to them. But now I have the following in each test classes’ setUp method.

<pre lang="python">self.cf = fmwrk_config.config()

Which gives me all my config information so I can put things in tests like

<pre lang="python">self.selenium.select_frame("RouteOneFrame")
self.selenium.type("Login.Token1", self.cf.users["id"]["username"])
self.selenium.type("Login.Token2", self.cf.users["id"]["password"])
self.selenium.click("realLogon")

So what are the advantages of all this?

  • It was fun; the advantage of which cannot be overstated
  • I can store all configuration information in one spot instead of embedding it in different places for different types of tests
  • And what has the most potential upside is that tests can now be grouped by function rather than by user as they currently are. Having tests grouped by user made them easy to debug, but spread the work out in a manner that didn’t always make sense