Okay. Maybe a Singleton wasn’t such a hot idea

For Page Objects to work, there needs to be a way for each class to get a reference to the current browser instance. When I first needed a single, globally available ‘thing’ I learned about the Singleton pattern and latched on it.

Yes, the Singleton pattern has a poor reputation in some circles but it worked well for almost two years since my scripts tend to be built around ideas that include not running things in parallel, not using the runner to manage execution distribution (through the use of something like Se-Grid) and using a single browser per stack.

A couple different conversations with customers [and potential ones] have made me rethink the first idea. It turns out that running a single browser, in parallel is perfectly in my ‘acceptable’ idea set. In fact, its a pretty good idea. [I know. Duh.] You can still have a single browser (controlled from a config file) that is distributed through a CI server — or better still into something like Sauce Labs’ OnDemand service.

But.

That doesn’t work in a world where you get a browser from a Singleton. For that, you need to use Dependency Injection. That is where you pass into the constructor of an object what it needs to know in order to behave. In the Page Object context we pass in the reference that this particular script has to the Se server.

And while a lot of this can be change can be hidden at the framework level, there is some user-side code changes as well.

If you are using Py.Saunter 0.39 or newer with WebDriver, you will have to make the following changes. The Se-RC implementation remains using the Singleton approach. SaunterPHP users will notice that this is how their WebDriver Page Objects have always been written.

  1. update conftest.py – conftest.py is a ‘magic’ file that gets used by Py.Saunter’s underlying runner (py.test) which can augment py.test’s behaviour at various points in the execution lifecycle. Remove the contents that came with the installation and replace them with the following.
    import py
     
    def pytest_runtest_makereport(__multicall__, item, call):
        if call.when == "call":
            try:
                assert([] == item._testcase.verificationErrors)
            except AssertionError:
                call.excinfo = py.code.ExceptionInfo()
        rep = __multicall__.execute()
        return rep
  2. modules/tailored/webdriver.py – While I was reworking how browsers get launched I made it a lot easier to add or customize WebDriver methods by putting in proper inheritance rather than just having static class methods.

    Create a new file called modules/tailored/webdriver.py with the following.

    """"
    =========
    WebDriver
    =========
    """
    from saunter.SaunterWebDriver import SaunterWebDriver
     
    class WebDriver(SaunterWebDriver):
        """
        Modifications to the core WebDriver API are done in this class
        """
        def __init__(self, **kwargs):
            super(WebDriver, self).__init__(**kwargs)

    This class is where you would add methods to handle your special control behaviours.

  3. de-static – As a result of SaunterWebDriver no longer being an object with a bunch of static methods on it, you need to change references from it to self.driver. So change
    SaunterWebDriver.is_element_present(locator):

    to

    self.driver.is_element_present(locator):
  4. self.driver (1) – Each Page Object should now be created with with a driver instance as it starting parameter. self.driver is set on a script when the browser is opened.
    @pytest.marks('shallow', 'ebay', 'shirts')
    def test_collar_style(self):
        s = ShirtPage()
        s.go_to_mens_dress_shirts()
        s.change_collar_style("Banded (Collarless)")
        assert(s.is_collar_selected("Banded (Collarless)"))

    becomes

    @pytest.marks('shallow', 'ebay', 'shirts')
    def test_collar_style(self):
        s = ShirtPage(self.driver)
        s.go_to_mens_dress_shirts()
        s.change_collar_style("Banded (Collarless)")
        assert(s.is_collar_selected("Banded (Collarless)"))

    If you miss a location is will throw and exception about an incorrect number of arguments.

  5. self.driver (2) – Page Objects likely already have self.driver being set in the __init__ method, but make sure that it now uses the passed in driver information.
    def __init__(self):
        self.driver = se_wrapper().connection
        self.config = cfg_wrapper().config

    vs.

    def __init__(self, driver):
        self.driver = driver
        self.config = cfg_wrapper().config
  6. elements – since Element classes are created at object instantiation time they don’t have access in their constructor to the session information. But they do have access to it through their ‘obj’ parameter. Which means to access the browser in an Element class you need obj.driver. Here is the before and after from an Element that gets clicked.
    def __set__(self, obj, val):
        e = SaunterWebDriver.find_element_by_locator(self.locator)
        e.click()
    def __set__(self, obj, val):
        e = obj.driver.find_element_by_locator(self.locator)
        e.click()

This is unfortunately not a trivial amount of code to change, but it is pretty straight forward and puts things into a nicer place.

I expect to publish the version of Py.Saunter with this change on Tuesday morning and I converted two projects in order to get the steps figured out but if you run into any problems, please email me and I’ll see if I can help you out.

Post a Comment

Your email is never published nor shared. Required fields are marked *