Skip to content
On this page

Custom Selenium Webdrivers using Decorators

The main question:

How to create custom selenium webdriver classes dynamically with a parametrized browser binary path?

The motivation:

Working on website functional testing using selenium webdriver, generates the urge to run tests using different versions of the most commonly used browsers these days, in parallel on the same machine.

Tests can use parametrized where the parameter can be the driver class itself, and here comes the need to generate drivers classes dynamically and pass them as parameters to the tests.

The idea:

To use more than one version of a browser, you first need to install these versions locally, then create a custom webdriver class generator with a custom binary path and use the generated classes in the tests without being initialized.

The procedure:

Below are the steps followed for the most popular browsers these days: Mozilla Firefox and Google Chrome.

  • Download Firefox versions you want to use in testing from Mozilla releases archive.

  • For Chrome it might take some effort, by the instructions are clear on the Chromium project site, I am going to list them here just in case they got missing:

    • Look in googlechromereleases.blogspot.com for the last time the wanted version was mentioned.
    • Loop up that version history (e.g.: “44.0.2403.157”) in the Position Lookup
    • In this case it returns a base position of “330231”. This is the commit of where the 44 release was branched, back in May 2015.*
    • Open the continuous builds archive
    • Click through on your platform (Linux/Mac/Win)
    • Paste “330231” into the filter field at the top and wait for all the results to XHR in.
    • Eventually I get a perfect hit: commondatastorage.googleapis.com/chromium-browser-snapshots
    • Sometimes you may have to decrement the commit number until you find one.
    • Download and run!
  • With all versions ready and able to run, you have several options for storing them system-wide. One option is to save them in environment variables.

  • Write a custom webdriver class generator, where the binary path is a parameter and can be dynamically changed in selenium tests. But the first problem I faced here was to be able to change the __init__ method for our custom class and pass the custom binary path to it. I found out that it can be done by deriving from the class type:

binary = FirefoxBinary(bin_path)
def __init__(self, firefox_binary=binary):
    webdriver.Firefox.__init__(self, firefox_binary=firefox_binary)

Firefox = type('Firefox', (webdriver.Firefox, ), {'__init__': __init__})
  • For google chrome, changing the binary path is done a little bit differently:
chrome_options = Options()
chrome_options.binary_location = bin_path

def __init__(self, chrome_options=chrome_options):
    webdriver.Chrome.__init__(self, chrome_options=chrome_options)

Chrome = type('Chrome', (webdriver.Chrome, ), {'__init__': __init__})
  • It would be more practical to put these functionality into decorators. For this purpose I wrote two decorators one to generate Firefox webdriver objects:
class CustomFirefox:

    def __init__(self, bin_path):
        self.bin_path = bin_path

    def __call__(self, cls):
        binary = FirefoxBinary(self.bin_path)

        def __init__(self, firefox_binary=binary):
            webdriver.Firefox.__init__(self, firefox_binary=firefox_binary)

        Firefox = type('Firefox', (webdriver.Firefox, ), {'__init__': __init__})
        return Firefox

and the other for Chrome webdriver objects:

class CustomChrome:

    def __init__(self, bin_path):
        self.bin_path = bin_path

    def __call__(self, cls):
        chrome_options = Options()
        chrome_options.binary_location = self.bin_path

        def __init__(self, chrome_options=chrome_options):
            webdriver.Chrome.__init__(self, chrome_options=chrome_options)

        Chrome = type('Chrome', (webdriver.Chrome, ), {'__init__': __init__})
        return Chrome
  • All you need to do is cast the appropriate decorator onto a class, pass the path to the custom binary. For example:
@CustomFirefox('<path_to_firefox_binary>')
class Driver: pass
    print(Driver)

The output would look like:

<class '__main__.Firefox'>

To open the browser, just call the newly created webdriver object:

Driver()
  • This works fine, except the original meta-data from the webdriver class are gone, so to keep the meta-data from the parent class (webdriver.Firefox or webdriver.Chrome) you need to save that data first before changing the __init__ method, then restore it afterwards. For example, for the Firefox decorator:
meta_data = dict(webdriver.Firefox.__dict__)
meta_data['__init__'] = __init__
Firefox = type('Firefox', (webdriver.Firefox, ), meta_data)

Now whe we check the returned class, we get:

<class 'selenium.webdriver.firefox.webdriver.WebDriver'>
``¨

- A little tweek might be helpful to include the default case where no path is passed as a parameter, so that the decorator generate the default webdriver with the default browser version installed on the system.

- Now it is possible to create a drivers factory to generate custom webdrivers from a list of binary paths, where the parameters to that factory are the decorator and the list of binary paths:

```python
def driver_factory(decorator, bin_paths):
    drivers = []
    for bin_path in bin_paths:
        @decorator(bin_path)
        class CustomDriver: pass
        drivers.append(CustomDriver)
return drivers
  • Voala! The webdriver is ready to be used in tests!

  • The complete script would look like:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.firefox.firefox_binary import FirefoxBinary


class CustomFirefox:
    '''
    Decorator to generate Firefox webdriver
    the parameter passed to this decorator is the path to
    the executable firefox browser
    '''
    
    def __init__(self, bin_path):
        self.bin_path = bin_path
    
    def __call__(self, cls):
        if self.bin_path:
            binary = FirefoxBinary(self.bin_path)
            
            def __init__(self, firefox_binary=binary):
                webdriver.Firefox.__init__(self, firefox_binary=firefox_binary)
            
            meta_data = dict(webdriver.Firefox.__dict__)
            meta_data['__init__'] = __init__
            Firefox = type('Firefox', (webdriver.Firefox, ), meta_data)
            return Firefox
        else:
            return webdriver.Firefox

        
class CustomChrome:
    '''
    Decorator to generate Firefox webdriver
    the parameter passed to this decorator is the path to
    the executable chrome browser
    '''
    
    def __init__(self, bin_path):
        self.bin_path = bin_path
    
    def __call__(self, cls):
        if self.bin_path:
            chrome_options = Options()
            chrome_options.binary_location = self.bin_path

            def __init__(self, chrome_options=chrome_options):
                webdriver.Chrome.__init__(self, chrome_options=chrome_options)

            meta_data = dict(webdriver.Chrome.__dict__)
            meta_data['__init__'] = __init__
            Chrome = type('Chrome', (webdriver.Chrome, ), meta_data)
            return Chrome
        else:
            return webdriver.Chrome


def driver_factory(decorator, bin_paths):
    drivers = []
    for bin_path in bin_paths:
        @decorator(bin_path)
        class CustomDriver: pass 
        drivers.append(CustomDriver)
    return drivers

It might be possible to do the same functionality using a meta class. I will look into that later. If you have any comments or suggestions please share it with me.

Go to Top