Saturday, December 22, 2012

PythonProject - Creating a Config Reader

One of the keys of good test framework design is creating a good system of managing configurations.  This allows your automated tests to be easily configurable to run on any environment against any target system.  If I want to run a test locally, I should be able to feed it a config file from my system and run the test against a local or QA instance, a Continuous Integration system should be able to feed it the config of the integration server and run it in CI.

USING THE OS ENVIRONMENT VARIABLES TO SPECIFY CONFIG FILES

Theres 3 ways you can basically 2 ways you can pass it config files.  As a run time parameter, an environment variable, or through some sort of scripting that copies the file into a hard coded location.  I decided to opt for the environment variable route.  This is easier for me to work with in my own dev environment as I can just type use a "export TEST_CONFIGS different_env.config" when I want to change config files without having to edit my build or shell scripts.

Another thing I thought might be nice is the ability to load multiple configs so I can split my settings across different config files.  For example, I can load the default configs that contain QA server settings by default.  But when I want to run the test across IT or Live, I can load those configs on top of the default settings so I overwrite the settings where applicable.

import os
...
        try:
            if(os.environ["TEST_CONFIGS"] == None):
                raise Exception("No Config Specified, using defaults.")
            else:
                # Read and load in all configs specified in reverse order
                configs = re.split(",|;", str(os.environ["TEST_CONFIGS"]))
                    self.__load_config_file(config)
        except:
            #Fall back to default.yaml file when no config settings are specified.
            self.__load_config_file(ConfigReader.DEFAULT_CONFIG_FILE_LOCATION)       
...


INTRODUCING YAML

Since our company is already using YAML for most of it's configurations anyways, I decided to also use YAML as the way of managing my configs.  The goal will be eventually I should be able to feed it config files from the server deployment along side to handle all the test environment references such as database connections and server base urls, then all I'll need to supply is an additional yaml files for managing test accounts, framework settings, and selenium configurations.

Implementing a config using YAML is pretty simple using PyYAML.  Using their classes, you can easily parse YAML files into a simple dictionary.
       
 import yaml
 ...
 config_yaml = open(config_file_location, 'r')  
 dataMap = yaml.load(config_yaml)  
 config_yaml.close()  
 ...  

IMPLEMENTING A NAME SPACED WAY OF ACCESSING CONFIGS

Since it would be annoying to have to type a bunch of quotes when accessing the python dictionary objects. "config_settings['selenium']['browser']['desiredcapabilities']".  I thought it be easier if I can reference settings in a name spaced way, "config.get("selenium.browser.desiredcapabilities"), especially for multilevel configs.  This can be easily done using split and a 'for' loop.


 ...
        def get_value(self,key):
        
            try:
                if "." in key:
                    #this is a multi levl string
                    namespaces = key.split(".")
                    temp_var = data_map
                    for name in namespaces:
                        temp_var = temp_var[name]
                    return temp_var
                else:
                    value = data_map[key]
                    return value                
            except (AttributeError, TypeError, KeyError):
                raise KeyError("Key '{0}' does not exist".format(key))
 ...  

While you're at it, it's a good idea to define a get_value method that returns a default when unavailable.


    def get_value_or_default(self,key, default):
        try:
            return self.get_value(key)
        except KeyError:
            return default

No comments: