While playing around with different design patterns in order to increase the efficiency of building a maintainable automation related architecture I’ve decided to give a chance also to the builder pattern. The main idea of it is to separate the construction of a test from its representation so the builder will implement the whole test logic, and a manager will provide to the user the representation. Like in real life right? (Developers implement features and a manager is praising).
Following this approach, you can create simple and readable tests, keeping at the same time the implementation logic structured and easy to maintain.
Step 1 – Builder interface
The first step to be done is the creation of an abstract interface used to define the product (test) components. We want our tests to have a prior validation, going further the test flow, a post validation, the reports, and the quit tests method. Apart from them we will add some logic in here related to the driver instantiation, a logic which will be used across all concrete builders.
class Builder(metaclass=abc.ABCMeta): """ Abstract interface for defining parts of the Product """ def __init__(self): self._driver = get_selenium_driver('chrome') self.product = Test() self._driver.get('https://en.wikipedia.org/') @abc.abstractmethod def get_pre_validation(self): pass @abc.abstractmethod def get_flow(self): pass @abc.abstractmethod def get_post_validation(self): pass @abc.abstractmethod def get_report(self, status): pass def close(self): self._driver.quit()
Here a test flow example will implement the Abstract Builder Interface methods. If we want other flows we just have to follow the same pattern, and change the methods implementation accordingly.
class SearchFlow(Builder): """ Implementation of a concrete builder """ SEARCH_CONTAINER = 'searchInput' SEARCH_BUTTON = 'searchButton' CREATE_ACCOUNT = 'pt-createaccount' LOGIN = 'pt-login' HEADING = 'firstHeading' def get_pre_validation(self): """ Validate if UI is available for the environment to be tested :return: True or False """ return self._driver.find_element_by_id('p-search').is_displayed() def get_flow(self): """ :return: True if validation succeeded """ self._driver.find_element_by_id(SearchFlow.SEARCH_CONTAINER).send_keys('Design patterns') self._driver.find_element_by_id(SearchFlow.SEARCH_BUTTON).click() def get_post_validation(self): """ :return: True if flow was executed """ return self._driver.find_element_by_id(SearchFlow.HEADING).text == 'Design pattern' def get_report(self, status): print("Test execution:", status)
Step 2 – The Manager
One of the key points in builder pattern definition is the Manager or Director or whatever you want to call it. It’s actually a class which will delegate the implementation details to the builders and provides the final product (in our case the test) to a client.
class TestManager: """ Control the construction process, having a builder associated with it It delegates to the builder the implementation details, and deliver the already implemented functionality to the client """ __builder = None __status = True def set_manager(self, builder): """ Selection of the builder which will construct the product :param builder: the concrete builder """ self.__builder = builder def get_test(self): """ Prepare the test product to be delivered to a client """ # Pre validation pre_validation = self.__builder.get_pre_validation() # Set flow if pre_validation: self.__builder.get_flow() # Make comparison if self.__builder.get_post_validation(): self.__status = True else: self.__status = False else: self.__status = False # Post results self.__builder.get_report(self.__status) # Close driver self.__builder.close()
Step 3 – Testing
Now that we are done with the implementation, let’s check the builder. Following the unit tests approach, we’ve declared a test_flow method, where we just have to instantiate the concrete builder (Searchflow), the manager (TestManager), send the builder to the manager and in the end our manager will provide the final test. Easy right?
class TestSearchFlow(TestTemplate): """ Test class for testing a search on Wikipedia """ def test_flow(self): """ Test Steps """ home_builder = SearchFlow() manager = TestManager() manager.set_manager(home_builder) manager.get_test()
Step 4 – Conclusions
As an endpoint, yes, it makes sense to use the builder pattern when designing an automation framework. You will have two big advantages: readable tests, maintainable implementation. The only downside which I found using this approach is that you could still have some code duplication while implementing the
get_flow method.Either way, this could be solved by combining this pattern with the classical page object.
More details about the implementation could be found here