Testing

Reaching object-oriented environmental elegance

Wrzesień 12, 2018 0
Podziel się:

Whether you are developing an ordinary enterprise application or your own little home-made web page, at some point you may decide to test it in different environments. And eventually, you will want to test it on as many environments as you can, before delivering it to production. At this point you can take different approaches. Some companies do beta testing to obtain feedback from actual users. But we will focus on a more traditional approach: automating the tests and running them on different environments.

An environment can be a browser, an operational system, a network set up, a screen, a device type, or anything/anyone else that can influence your app from outside. So, keeping in mind the complexity and the wide scope of all of these topics, how can we approach the task of automated testing and not let it turn into a complete mess?

The topic is quite big, but for now, we are going to focus on framework architecture, because it in itself can give us a number of answers.

First of all, let us decide on how to represent an environment inside your framework. Many literature sources about Python or JavaScript commonly suggest us to present everything as an object. This approach is smart. So, what if we make everything an object in a testing framework?

The environments have a nasty trait of being similar, but still, they are quite different, so we should keep this in mind and find a simple way of adjusting to the fact. In order to do so, we need to remind ourselves that a class is a description of some abstract, and an object is an instance of this abstraction. Each instance is unique, which allows us to create unique configurations of environments. So our goal here is to define the common traits of environments, and at the same time, to leave them adjustable to different settings.

The following python code shows how this abstraction looks like:


# create a class that represents an environment:
 class Environment(object):
    # class constructor accepts parameters that set it up:
    def __init__(self, setting, another_setting):
        self.setting = setting
        self.another_setting = another_setting

    # environment class contains methods that allow to configure
    # it using given settings:
    def configure_environment(self):
        do_something(self.setting)

# create one environment: env_setup = Environment(setting=”red”, another_setting=”failure”)
# create another environment:
env_setup2 = Environment(setting=”green”, antoher_setting=”success”)

The example above is just a sample, but it shows the main principle of configuring your environment. Now, let us go a little bit deeper and observe this on a real-life situation.

We have already managed to deal with configuration, now let us see how to deal with multiple environments. Imagine that you are going to test a cross-platform application on different operating systems. The app looks more or less the same on each platform, though you have to access it in different ways, using different drivers. In such situation, it is good to remember that everything can be an object, and each instance of an object is unique. Therefore, you will probably want to add 2 new drivers and create their objects when needed.


class MacOsDriver(object):
    def __init__(self, setting1, setting2):
        # do some settings

    def trigger_some_action(self):
        # do some settings

class WinDriver(object):
    def __init__(self, setting1, setting2):
        # do some settings

    def trigger_some_action(self):
        # do some settings

mac_os_driver = MacOsDriver(“setting1”, “setting2”)
win_driver = WinDriver(“setting1”, “setting2”)

While writing the tests, you will rather want to use a unified object without differentiating between drivers for the 2 platforms. So now, let us make it elegant by using an Object factory pattern. The main idea is that you encapsulate the object creation and conditionally return the relevant one without caring about the implementation inside.


# In this example “configurations” is some dictionary that contains settings for current
# test run. Example:
# {“os”: “MacOS”, “user”: “admin”, ... }
def get_driver(configurations, setting1, setting2):
    if configuration[“os”] == “MacOS”:
        return MacOsDriver(setting1, setting2)
    elif configuration[“os”] == “Win”:
        return WinDriver(setting1, setting2)

Now, you have a function that returns the corresponding object. To use it in tests you can simply call it and pass the returned value to a variable or a class attribute:


import unittest
# in this example we’re using unittest package that allows to create pre/post
# conditions and identify test cases:
class TestTheApp(unittest.TestCase):
     # the following method will be run before all tests inside
     # this class, which makes it a good place to define objects needed in the tests
     def setUpClass(self):
         super().setUpClass()
         configuration = {“os”: “Win”}
         self.driver = get_driver(configuration, setting1, setting2) 
     # test case:
     def test_some_use_case(self):
         self.driver.trigger_some_action()

Using the object factory is an easy way to deal with conditional environments usage. You can use it in cases of cross-browser testing (creating different instances of selenium web driver), mobile testing, connecting to different addresses or databases etc.

Of course, as any other approach, this one has its cons.

First of all, it requires that the driver objects must have the same structure to avoid attribute errors or missing methods when using them in the tests. Because of that, it is usually required to implement an abstract class or an interface that will take care of proper implementation of the actual drivers. In Python you can do that the following way:


class BaseOsDriver(object):
    def __init__(self, setting1, setting2):
        # do some settings 
    def trigger_some_action(self):
        raise NotImplementedError()

class WinDriver(BaseOsDriver):
    def __init__(self, setting1, setting2):
        super().__init__(setting1, setting2)

    def trigger_some_action(self):
        # do some win-specific actions

class MacOsDriver(BaseOsDriver):
    def __init__(self, setting1, setting2):
        super().__init__(setting1, setting2)

    def trigger_some_action(self):
        # do some os-specific actions

Another сomplication might appear when handling the driver constructor parameters. Sometimes various environments require a wide scope of different mandatory parameters, even though they must implement the same abstract class. In this case it is a good idea to wrap these settings into an object or a simple dictionary. This way it is going to be convenient to pass them into the object factory and will not cause any trouble with drivers implementation.

Taken into account the cons mentioned above, you will get the following code:


# driver_configurations.py
class BaseOsDriver(object):
    def __init__(self, settings_object):
        # do some settings

    def trigger_some_action(self):
        # raise an error if method was not implemented in child classes
        raise NotImplementedError()

# inherit Win and MacOS from base driver to control that all of the mandatory methods
# are implemented:
class WinDriver(BaseOsDriver):
    def __init__(self, settings_object):
        super().__init__(settings_object)

    def trigger_some_action(self):
        # do some win-specific actions

class MacOsDriver(BaseOsDriver):
    def __init__(self, settings_object):
        super().__init__(settings_object)

    def trigger_some_action(self):
        # do some os-specific actions

# object_factory.py:
from driver_configurations import WinDriver, MacOsDriver

def get_driver(configurations, settings):
    if configuration[“os”] == “MacOS”:
        # extend general settings with Mac OS specific ones:
        settings[“mac_os_specific_setting1”] = “some value”
        settings[“mac_os_specific_setting2”] = “some value”
        return MacOsDriver(common_settings)
    elif configuration[“os”] == “Win”:
        # extend general settings with Win specific ones:
        settings[“win_specific_setting1”] = “some value”
        return WinDriver(settings)

# test_configurations.py
from object_factory import get_driver

class TestTheApp(unittest.TestCase):
    def setUpClass(self):
        super().setUpClass()
        configuration = {“os”: “Win”}
        common_settings = {
            “url”: “https://some_website.com”,
            ...
        }
        # pass which configuration to use during current test run as “configuration”
        # pass some settings that are common for win and mac os as “common_settings”
        self.driver = get_driver(configuration, common_settings)

    # test case:
    def test_some_use_case(self):
        self.driver.trigger_some_action()

When used carefully, the object oriented approach makes your framework structured and very human-readable. If you use the simple approaches listed above, you will easily find an elegant way of handling different environments.

I wish you a happy coding and beauty in your creations! 🙂

4.9 / 5
Kategorie: Testing
Anastasiia Naboikina
Autor: Anastasiia Naboikina
Originally I am from Ukraine, but I have moved to Poland 5 years ago. Before my study in the University I was more into foreign languages than computer science, but a bit of luck and courage gave me this wonderful opportunity to try myself in automated testing. Now I work not only with test automation, but also with web development. I've been working at Sii as Senior Test Development Engineer since 2017, testing intelligent homes systems. My previous experience is connected with cloud-based technologies (SaaS, IaaS) and medical devices testing. Overall I have more than 7 years of experience in test automation. My favorite programming languages are Python and JavaScript. I also find myself very interested in various infrastructure related tasks, such as setting up Jenkins and Dockerized deployments. And... I have lots of hobbies :) I compose music, sing, play keyboard (mostly jazz), paint, play mahjong and do different sports.

Imię i nazwisko (wymagane)

Adres email (wymagane)

Temat

Treść wiadomości

Zostaw komentarz