UI Automation
UI automation using Selenium getting a good buzz since selenium had come up with Webdriver which provides useful and essential methods, properties and frameworks to automate web UI. Programming languages like Java, Python, C#, etc. are widely used with selenium to automate web applications. In this article, we are going to talk about how python can be used with selenium web driver for creating test automation scripts.
How to build an automation framework
A framework is basically a way of creating a kind of prototype to structure and organize your test automation scripts so that they can be easily run and maintained throughout the lifecycle of your test.
Python provides various inbuilt modules with the help of which an efficient framework can be built using selenium. 
  1. Unittest
  2. Pytest
  3. Page object model
  4. Data driven testing
  5. Behaviour driver development (BDD)
Using a combination of all the above framework techniques, hybrid automation framework can be created by following below standards,
  • All the generic methods or static methods that are reusable in nature irrespective of your web application can be created under a class called base class and should be organized in the 'Base' directory of your project.
  • Page object model feature enables you to create a test script of every individual web page of your web application and such test scripts can be organized in a directory/package called Page.
  • Test methods should be created for all your test cases under test class which can be stored in the 'Tests' directory/package of your framework.
  • All the screenshots captured from the test methods should be saved and stored in the 'Screenshots' directory/package of your framework.
  • All your behavior-driven tests (business use cases/scenarios) should be kept and stored under the 'BDD' directory/package.
  • While using pytest you need to make sure that all your test methods should follow certain naming conventions like 
  • All the test methods should start with 'test' keyword.
  • The class name should start with the 'test' keyword. The typical folder structure should look like:
Framework structure 
  • For BDD using behave, a feature file should be created containing use cases/business scenarios in the below format and this feature file should be kept under Feature folder/directory.
        Feature:
                  Scenario:
                         Given:
                         When:
                         Then:
  • steps directory should contain steps.py file containing step implementation for your scenario defined in the feature file.
              BDD
                 ---features
                      --steps
                          ---steps.py
                     --<filename>.feature
  • Various reports can be generated for test runs like the Html report, Allure report to capture test results.
  • With the help of the inbuilt logging module, logs can be generated for required events/actions.
Base class
The base class contains generic and reusable methods and these methods are nothing but wrappers developed on top of selenium webdriver classes and methods.
The following code depicts a base class having few generic, static and reusable methods defined.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.wait import WebDriverWait
from selenium.common.exceptions import *
import utilities.logger as cl
import inspect
from traceback import print_stack
import time
from datetime import datetime
import os
from selenium.webdriver.support.ui import Select
from selenium.webdriver import ActionChains
import logging


# This is a base class
class SeleniumDriver(): """
 This is a base class containing all the generic/reusable and static methods
 """

 # Object of logger class
    log_base = cl.customlogger(loglevel=logging.DEBUG)

    # Constructor to initialize driver    def __init__(self, driver):
        self.driver = driver

    def get_by_type(self, locatorType): """
 This method will return the by type for the locatorType argument.
 =================================================================
 Parameter:
 ----------
 1. Required:
   1. locatorType: [id, name, class, link-text, xpath, css, tag etc.]
 Return:
 -------
   It returns By locator types e.g. By.ID, By.NAME etc.
 Exception:
 ----------
   Error if locatorType is not correct/supported
 """
        log_base = cl.customlogger(loglevel=logging.DEBUG)
        locatorType = locatorType.lower()
        if locatorType == 'id':
            return By.ID
        if locatorType == 'name':
            return By.NAME
        if locatorType == 'class':
            return By.CLASS_NAME
        if locatorType == 'link-text':
            return By.LINK_TEXT
        if locatorType == 'xpath':
            return By.XPATH
        if locatorType == 'css':
            return By.CSS_SELECTOR
        if locatorType == 'tag':
            return By.TAG_NAME
        else:
            self.log_base.info("[FAIL] Test method '{0}-->{1}".format(__name__,
                           inspect.currentframe().f_code.co_name) + "Locator type " + locatorType +
                          " not correct/supported")
        return False
    def get_element(self, locator, locatorType="id"):
 """
 This method will find out an element according to the combination of locator and locatorType.
 =============================================================================================
 Parameter:----------
 1. Required
   1. locator
   2. locatorType
 Return:
 -------
   Element
 Exception:
 ----------
   NoSuchElementException
 """
        element = None        try:
            locatorType = locatorType.lower()
            byType = self.get_by_type(locatorType)
            element = self.driver.find_element(byType, locator)
            self.log_base.info("[PASS] Test method: '{0}-->{1}'".format(__name__,
                      inspect.currentframe().f_code.co_name) + "Element found with locator: " +
                       locator + " and  locatorType: " + locatorType)
        except:
            self.log_base.error("[ERROR] Test method: '{0}{1}'".format(__name__,
             inspect.currentframe().f_code.co_name) + "Element not found with locator: " + 
            locator + " and locatorType: " + locatorType +" ###Exception: {}".format(NoSuchElementException))
        return element

    def get_element_list(self, locator, locatorType="id"):


 """
 This method is useful for fetching multiple elements from parent element
 ========================================================================
 Parameter:
 ----------
 1. Required:
   1. locator
   2. locatorType
 Return:
 -------
   It returns list of elements
 Exception:
 ----------
   Element list NOT FOUND error
 """        locatorType = locatorType.lower()
        byType = self.get_by_type(locatorType)
        elements = self.driver.find_elements(byType, locator)
        if len(elements) > 0:
            self.log_base.info("[PASS] Test method: '{0}-->{1}'".format(__name__,
                         inspect.currentframe().f_code.co_name) + "Element list FOUND with locator: " +
                         locator + " and locatorType: " + locatorType)
        else:
            self.log_base.info("[FAIL] Test method: '{0}-->{1}'".format(__name__,
                          inspect.currentframe().f_code.co_name) + "Element list NOT FOUND with locator: " +
                          locator + " and locatorType: " + locatorType)
        return elements

    def get_element_type(self, locator, locatorType): """
 This method will return type of an element.
 ===========================================
 Parameter:
 ----------
 1. Required
   1. locator
   2. locatorType
 Return:
 -------
   It returns element_type.
 Exception:
 ----------
   None
 """
        element = self.get_element(locator, locatorType)
        element_type = element.get_attribute("type")
        if element_type == 'text':
            return element_type
        if element_type == 'radio':
            return element_type
        if element_type == 'checkbox':
            return element_type
        if element_type == 'submit':
            return element_type

    def driver_close(self):
      """
    Driver instance will be closed
    :return: It doesn't return anything
    """
        self.driver.close()
        self.log_base.info("[PASS] Driver is successfully closed using test method:"'{0}-->{1}'".
                   format(__name__, inspect.currentframe().f_code.co_name))

    def sendKeys(self, data, locator, locatorType="id", element=None): """
 Send keys to an element -> MODIFIED
 Either provide element or a combination of locator and locatorType
 """
        try:
            if locator:  # This means if locator is not empty
                element = self.get_element(locator, locatorType)
            element.send_keys(data)
            self.log_base.info("[PASS] Sent data '{0}' on element with locator: ".format(data)
                          + locator + " locatorType: " + locatorType +
                          "using test method '{0}-->{1}'" format(__name__, inspect.currentframe().f_code.co_name))
        except:
            self.log_base.error("[FAIL] Cannot send data '{0}' on the element with locator: "
           .format(data) + locator + " locatorType: " + locatorType + "using test method ""'{0}-->{1}'"
          .format(__name__, inspect.currentframe().f_code.co_name))
            print_stack()

    def element_click(self, locator, locatorType="id", element=None):    """
    Click on an element -> MODIFIED
    Either provide element or a combination of locator and locatorType
    """
        try:
            if locator:  # This means if locator is not empty
                element = self.get_element(locator, locatorType)
            element.click()
            self.log_base.info("[PASS] Clicked on element with locator: " + locator +
               " locatorType: " + locatorType + "using test method '{0}-->{1}'".format(__name__,
                inspect.currentframe().f_code.co_name))
        except:
            self.log_base.error("[FAIL] Cannot click on the element with locator: " + locator +
                  " locatorType: " + locatorType + "using test method '{0}-->{1}'".format(__name__,
                   inspect.currentframe().f_code.co_name))
            print_stack()

    def capture_screenshot(self, resultMessage):   """
   Takes screenshot of the current open web page
   """
        now = datetime.now()
        fileName = resultMessage + "." + \
            str(now.strftime("%Y%m%d%H:%M:%S %p")) + ".png"

        screenshotDirectory = "/Users/pravin.a/my-project/venv/MyFramework/screenshots/{0}{1}{2}".
             format("screenshots", "_", str(now.strftime("%Y%m%d")))
        relativeFileName = fileName
        currentDirectory = os.path.dirname(__file__)
        destinationFile = os.path.join(screenshotDirectory, relativeFileName)
        destinationDirectory = os.path.join(
            currentDirectory, screenshotDirectory)

        try:
            if not os.path.exists(destinationDirectory):
                os.makedirs(destinationDirectory)
            self.driver.save_screenshot(destinationFile)
            self.log_base.info("[PASS] Test method: '{0}-->{1}'".format(__name__,
                inspect.currentframe().f_code.co_name) + ":" + " Screenshot save to directory:"+ destinationFile)
        except:
            self.log_base.error("[FAIL] Test method: '{0}-->{1}'".format(__name__,
                  inspect.currentframe().f_code.co_name) + "### Exception Occurred when taking screenshot")
            print_stack()

    def get_title(self):
       """
     This method returns title of the current page.
     """
        return self.driver.title

    def clear_field(self, locator, locatorType="id"):
        """
     This method will Clear an element field.
     """
        element = self.get_element(locator, locatorType)
        element.clear()
        self.log_base.info("[PASS] Test method: '{0}-->{1}'".format(__name__,
                 inspect.currentframe().f_code.co_name) + "Clear field with locator: " +locator + 
                " locatorType: " + locatorType)

    def get_text(self, locator, locatorType="id", element=None, info=""):
    """
   NEW METHOD
   Get 'Text' on an element
    Either provide element or a combination of locator and locatorType        """        try:
            if locator:  # This means if locator is not empty
                element = self.get_element(locator, locatorType)
            text = element.text
            if len(text) == 0:
                text = element.get_attribute("innerText")
            if len(text) != 0:
                self.log_base.info("[PASS] Test method: '{0}{1}'".format(
                    __name__, inspect.currentframe().f_code.co_name) + "Getting text on element ::"+ info)
                self.log_base.info("The text is :: '" + text + "'")
                text = text.strip()
        except:
            self.log_base.error("[FAIL] Test method: '{0}{1}'".format(
               __name__, inspect.currentframe().f_code.co_name) + "Failed to get text on element"+ info)
            print_stack()
            text = None        return text

    def is_element_present(self, locator, locatorType="id", element=None):   """
   Check if element is present -> MODIFIED
   Either provide element or a combination of locator and locatorType
        """
        try:
            if locator:  # This means if locator is not empty
                element = self.get_element(locator, locatorType)
            if element is not None:
                self.log_base.info("[PASS] Test Method: '{0}-->{1}'".format(__name__,
                    inspect.currentframe().f_code.co_name) + "Element present with locator: "+ locator +
                    " locatorType: " + locatorType)
                return True            else:
                self.log_base.error("[FAIL] Test method: '{0}-->{1}'".format(__name__,
                     inspect.currentframe().f_code.co_name) + "Element not present with locator: "+ locator + 
                   " locatorType: " + locatorType)
                return False        except:
            print("[FAIL] Test method: '{0}-->{1}'".format(__name__, inspect.currentframe()
                 .f_code.co_name) + "Element not found" + "###Exception-->{}".format
                 (NoSuchElementException))
            return False
    def is_element_displayed(self, locator, locatorType="id", element=None):   """
   NEW METHOD
   Check if element is displayed
   Either provide element or a combination of locator and locatorType
   """
        isDisplayed = False        try:
            if locator:  # This means if locator is not empty
                element = self.get_element(locator, locatorType)
            if element is not None:
                isDisplayed = element.isDisplayed()
                self.log_base.info("[PASS] Test method: '{0}-->{1}'".format(
                    __name__, inspect.currentframe().f_code.co_name) + "Element is displayed")
            else:
                self.log_base.info("[FAIL] Test method: '{0}-->{1}'".format(
                    __name__, inspect.currentframe().f_code.co_name) + "Element is not displayed")
            return isDisplayed
        except:
            print("[ERROR] Test method: '{0}-->{1}'".format(__name__,
                     inspect.currentframe().f_code.co_name) + "Element not found" +
                  "###Exception-->{}".format(ElementNotVisibleException))
            return False
    def wait_for_element(self, locator, locatorType="id",
                         timeout=10, pollFrequency=0.5):   """
   This method will make webdriver to wait for a particular amount of time.
   """
        element = None        try:
            byType = self.get_by_type(locatorType)
            self.log_base.info("Waiting for maximum :: " + str(timeout) +
                          " :: seconds for element to be clickable")
            wait = WebDriverWait(self.driver, timeout=timeout,
                                 poll_frequency=pollFrequency,
                                 ignored_exceptions=[NoSuchElementException,
                                                     ElementNotVisibleException,
                                                     ElementNotSelectableException])
            element = wait.until(EC.element_to_be_clickable((byType, locator)))
            self.log_base.info("[PASS] Test method '{0}-->{1}'".format(__name__,
                      inspect.currentframe().f_code.co_name) + "Element appeared on the web page" +
                      "with locator" + locator + "and locatortype" + locatorType)
        except:
            self.log_base.info("[FAIL] Test method '{0}-->{1}'".format(__name__,
                  inspect.currentframe().f_code.co_name) + "Element not appeared on the web page"
                  + "with locator" + locator + "and locatortype" + locatorType)
            print_stack()
        return element

    def element_presence_check(self, locator, byType):   """
   Check if element is present
   =============================
   Parameter:
   ----------
   Required:
     1. locator
     2. byType
   Return:
   -------
     If element present --> Returns True
     If element absent --> Returns False
   Exception:
   ----------
     NoSuchElementException
   """
        try:
            elementList = self.driver.find_elements(byType, locator)
            if len(elementList) > 0:
                self.log_base.info("[PASS] Test method '{0}-->{1}'".format(__name__,
                      inspect.currentframe().f_code.co_name) + "Element present with locator: "+ locator + 
                      " locatorType: " + str(byType))
                return True            else:
                self.log_base.info("[FAIL] Test method '{0}-->{1}'".format(__name__,
                     inspect.currentframe().f_code.co_name) + "Element not present with locator: "+ locator + 
                    " locatorType: " + str(byType))
                return False        except:
            self.log_base.info("[ERROR] Test method: '{0}-->{1}'".format(__name__,
                    inspect.currentframe().f_code.co_name) + "Element not found," +"###Exception-->{}".
                    format(NoSuchElementException))
            return False
    def web_scroll(self, direction="up"):   """
   NEW METHOD
   """
        if direction == "up":
            # Scroll Up            self.driver.execute_script("window.scrollBy(0, -800);")

        if direction == "down":
            # Scroll Down            self.driver.execute_script("window.scrollBy(0, 600);")

    def switch_to_frame(self, id="", name="", index=None): """
 Switch to iframe using element locator inside iframe

 Parameters:
 1. Required:
    Either id, name or index
 Returns:
   None
 Exception:
   None        """

        if id:
            self.driver.switch_to.frame(id)
            self.log_base.info("Test Method: '{0}{1}'".format(__class__, inspect.currentframe(
            ).f_code.co_name) + "Controll is switched to frame with id:  '{}'".format(id))
        elif name:
            self.driver.switch_to.frame(name)
            self.log_base.info("Test Method: '{0}{1}'".format(__class__, inspect.currentframe(
            ).f_code.co_name) + "Controll is switched to frame with name:  '{}'".format(name))
        else:
            self.driver.switch_to.frame(index)
            self.log_base.info("Test Method: '{0}{1}'".format(__class__, inspect.currentframe(
            ).f_code.co_name) + "Controll is switched to frame with index '{}'".format(index))

    def switch_to_window(self):
        parent_handle = self.driver.current_window_handle
        all_handles = self.driver.window_handles
        try:
            for handle in all_handles:
                if handle not in parent_handle:
                    self.driver.switch_to.window(handle)
                    self.log_base.info("Test Method: '{0}{1}'".format(__class__, inspect.currentframe(
                          ).f_code.co_name) + "switched to a new window '{}'".format(handle))
        except:
            self.log_base.error("Test Method: '{0}{1}'".format(__class__, inspect.currentframe(
                ).f_code.co_name) + "Invalid window handle, control can't be switched to the "handle:")
        return parent_handle


    def switch_to_default_content(self, parent_handle): """
 Switch to default content
 Parameters:
 1. Required:
     1. parent_handle
 Returns:
    None
 Exception:
    None
 """
        self.driver.switch_to.window(parent_handle)
        #self.driver.switch_to_default_content()

        self.log_base.info("Test Method '{}{}'".format(__class__, inspect.currentframe(
        ).f_code.co_name) + "Control is switched back to default content")

    def alert_popups(self, choice):
        alert_popup = self.driver.switch_to.alert
        if choice in ['ok', 'Ok', 'OK']:
            alert_popup.accept()
        elif choice in ['cancel', 'Cancel', 'CANCEL']:
            alert_popup.dismiss()
        else:
            self.log_base.error("Test Method '{}{}'".format(__class__, inspect.currentframe(
        ).f_code.co_name) + " " + "Invalid choice")


    def get_element_attribute_value(self, attribute, locator="", locatorType="", element=None):

 """
 Get value of the attribute of element
 Parameters:
 1. Required:
     1. attribute - attribute whose value to find
 2. Optional:
    1. element - Element whose attribute need to find
    2. locator - Locator of the element
    3. locatorType - Locator Type to find the element
 Returns:
 Value of the attribute
 Exception:
 None
 """
        if locator:
            element = self.get_element(
                locator=locator, locatorType=locatorType)
        value = element.get_attribute(attribute)
        if value is not None:
            self.log_base.info("Test Method '{0}{1}'".format(__class__, inspect.currentframe(
            ).f_code.co_name) + "Attribute value is: {} ".format(value))
        else:
            self.log_base.warn("Test Method '{0}{1}'".format(__class__, inspect.currentframe(
            ).f_code.co_name) + "Attribute value is not returned" + "Make sure an element you 
             are looking for is a valid element")
        return value

    def is_enabled(self, locator, locatortype="id", info=""): """
 Check if element is enabled

 Parameters:
 1. Required:
    1. locator - Locator of the element to check
 2. Optional:
    1. locatorType - Type of the locator(id(default), xpath, css, className, linkText)
    2. info - Information about the element, label/name of the element
 Returns:
 boolean
 Exception:
 None
 """
        element = self.get_element(locator, locatorType=locatortype)
        enabled = False        try:
            attributeValue = self.get_element_attribute_value(
                element=element, attribute="disabled")
            if attributeValue is not None:
                enabled = element.is_enabled()
            else:
                value = self.get_element_attribute_value(
                    element=element, attribute="class")
                self.log_base.info(
                    "Attribute value From Application Web UI --> :: " + value)
                enabled = not ("disabled" in value)
            if enabled:
                self.log_base.info("Element :: '" + info + "' is enabled")
            else:
                self.log_base.info("Element :: '" + info + "' is not enabled")
        except:
            self.log_base.error("Element :: '" + info +
                           "' state could not be found")
        return enabled

    def select_option(self, locator, locatorType="id", index=None, text=None,
                      value=None, element=None): """
 This method will select an required option/item/element from select element.
 Parameters:
 1. Required:
   1. locator
   2. locatorType
   3. Either index Or text Or value
 2. Optional:
   1. element
 Returns:
 It doesn't return anything
 Exception:
 None
 """
        if locator:
            element = self.get_element(locator, locatorType=locatorType)
        select = Select(element)
        if index is not None:
            select.select_by_index(index)
            self.log_base.info('Test Method:"{0}{1}"'.format(__class__, inspect.currentframe
                          ().f_code.co_name) + "is successful. " + "Element is selected successfully")
        elif text is not None:
            select.select_by_visible_text(text)
            self.log_base.info('Test Method:"{0}{1}"'.format(__class__, inspect.currentframe
                   ().f_code.co_name) + "is successful. " + "Element '{}' is selected successfully".format(text))
        elif value is not None:
            select.select_by_value(value)
            self.log_base.info('Test Method:"{0}{1}"'.format(__class__, inspect.currentframe
                                          ().f_code.co_name) + "is successful. " + "Element '{}' is selected successfully".
                               format(value))
        else:
            print("Invalid argument to select method")
            self.log_base.error("Test method: '{0}{1}'".format(__class__, inspect.currentframe
                                 ().f_code.co_name) + "is failed. " + "Unable to select an item" + "'{}'".
                                format('"Invalid argument to select method"'))

    def deselect_all(self, locator, locatorType): """
 This method will deselect all the selected items/options from the select element.
 Parameter:
 1. Required:
   1. locator
   2 locatorType
 2. Optional:
   None
 Return:
 element [*Note - This is optional]
 Exception:
 None
 """
        element = self.get_element(locator, locatorType=locatorType)
        select = Select(element)
        select.deselect_all()
        self.log_base.info("Test Method '{0}{1}'".format(
          __class__, inspect.currentframe().f_code.co_name) + "Elements are deselected")
        return element

    def get_all_selected_options(self, locator, locatorType):
        element = self.get_element(locator, locatorType=locatorType)
        select = Select(element)
        allSelectedOptions = select.all_selected_options
        return allSelectedOptions, element

    def get_all_options(self, locator="", locatorType="id", element=None): """
 This method will find out and return all the options from Select element.
 Parameter:
 1. Required:
   1. locator
   2 locatorType
 2. Optional:
   1. element
 Return:
 None
 Exception:
 None
 """
        if locator:
            element = self.get_element(locator, locatorType=locatorType)
            select = Select(element)
            options = select.options
            return options
        if element:
            select = Select(element)
            options = select.options
            return options

    def drag_and_drop(self, source, target): """
 This method will drag the source element to a target/destination element.
 Parameter:
 1. Required:
   1. source - source from which files are required to be copied.
   2. target - target at which files needs to be copied.
 2. Optional:
  None
 Return:
  None
 Exception:
  None
 """
        action_chains = ActionChains(self.driver)
        # Disable below line of code if doesn't work.
        action_chains.drag_and_drop(source, target).perform()

        # Enable below line of code if above code doesn't work.
        action_chains.click_and_hold(source).release(target).perform()

    def file_upload(self, locator, locatorType, file_path, element=None): """
 This method will upload the required file at the desired location.
 Parameter:
 1. Required:
   1. locator
   2. locatorType
   3. file_path
 2. Optional:
   1. element
 Return:
   None
 Exception:
   None
 """
        if locator:
            element = self.get_element(
                locator=locator, locatorType=locatorType)
            element.send_keys(file_path)
        else:
            self.sendKeys(data=file_path, element=element, locator="")

    def get_data_web_table(self, locator, locatorType="id", element=None):
  """
 :param locator: locator to find out an element
 :param locatorType: default locator type is 'id'
 :param element: By default element is set to None and it will be set to the value as passed through the script.
 :return: It returns a list of cell values from the web table
 """
        web_table_list = []
        if locator:
            table = self.get_element(locator, locatorType)
            element = table
        for row in element.find_elements(By.XPATH, ".//tr"):
            for cell in row.find_elements(By.XPATH, './/td'):
                web_table_list.append(cell.text)
        return web_table_list
 =================================================================================
Similarly, you can create generic methods for your UI to be tested under this base class.

Page Class
Page class contains actual automated code for your test for the required page, screen.
E.g. There is a page in your web application which deals with alerts and popups. The below code shows how alerts are handled in page class.
Similarly, you can create automation scripts for your various web pages.
import utilities.logger as cl
import logging
from base.basepage import BasePage
from page.navigation.navigation_page import NavigationPage
from utilities.util import Util

class LoginPage(BasePage):

    log_base = cl.customlogger(loglevel=logging.DEBUG)

    def __init__(self, driver):
        super().__init__(driver)
        self.driver = driver
        self.nav = NavigationPage(driver)
        self.util = Util()

    def _click_login_link(self):
        self.element_click(self.util.get_locator('locators', 'login_link'), locatorType="link-text")

    def _enter_email(self, email):
        self.wait_for_element(self.util.get_locator('locators', 'email_field'), locatorType='xpath')
        self.sendKeys(data=email, locator=self.util.get_locator('locators', 'email_field'), locatorType='xpath')

    def _enter_password(self, password):
        password_field = self.wait_for_element(locator=self.util.get_locator('locators', 'password_field'), locatorType='xpath')
        self.sendKeys(data=password, element=password_field, locator="")

    def _click_login_button(self):
        self.wait_for_element(self.util.get_locator('locators', 'login_button'), locatorType="name")
        self.element_click(self.util.get_locator('locators', 'login_button'), locatorType="name")

    def login(self, email, password):
        self._click_login_link()
        self._enter_email(email)
        self._enter_password(password)
        self._click_login_button()

    def verify_login_successful(self):
        self.wait_for_element("My Courses".strip(), locatorType="link-text")
        result = self.is_element_present("My Courses".strip(), locatorType="link-text")
        return result

    def verify_login_failed(self):
        result = self.is_element_present("//div[contains(text(),'Invalid email or password')]", locatorType="xpath")
        return result

    def verify_login_title(self):
        return self.verifyPageTitle("Let's Kode It")

    def logout(self):
        self.nav.navigate_to_user_settings()
        self.element_click(locator="//div[@id='navbar']//a[@href='/sign_out']", locatorType="xpath")
Test Class
Now that you have created a generic method in Base class and you also have an automation test script for your page in page class, let's create a test method to execute a relative test case.
from page.login.login_page import LoginPage
from utilities.teststatus import TestStatus
from utilities.util import Util
from page.login.login_page import LoginPage
import page.login.login_page
import unittest
import pytest
import HtmlTestRunner

@pytest.mark.usefixtures("oneTimeSetUp", "setUp")
class LoginTests(unittest.TestCase):

    @pytest.fixture(autouse=True)
    def object_setup(self, oneTimeSetUp):
        self.login_page = LoginPage(self.driver)
        self.test_status = TestStatus(self.driver)
        self.util = Util()

    @pytest.mark.run(order=1)
    def test_validLogin(self):
        self.login_page.login(self.util.get_locator('credentials','username'),
        self.util.get_locator('credentials','password'))

        page_title_check = self.login_page.verify_login_title()
        if page_title_check:
            self.login_page.capture_screenshot("Login title is verified")
        self.test_status.mark(page_title_check, "Title Verification")
        login_status = self.login_page.verify_login_successful()
        if login_status:
            self.login_page.capture_screenshot("Login is successful")
            self.test_status.mark_final("test_validLogin", login_status, "Login Verification")

    @pytest.mark.run(order=2)
    def test_invalidLogin(self):
        self.login_page.logout()
        self.login_page.login(self.util.get_locator('credentials','invalid_username'),
        self.util.get_locator('credentials','invalid_password'))
        result = self.login_page.verify_login_failed()
        assert result == True

Conftest.py
Conftest.py file is used to define all the fixtures that are referenced by the testscripts during run time. pytest will look for the conftest.py file in the project directory and it will execute all the fixtures as soon as the test class is invoked. There are some class level fixtures and test level fixtures as defined in the below code. 'oneTimeSetUp' fixture will create driver instance for the test. It will accept certainparameters/arguments like browser, url from the command line and then it will be passed to thedriver instance. Similarly setUp method defined below is a fixture that is applicable for every test method i.e. it will be executed before any test method runs.
import pytest
from base.webdriverfactory import WebDriverFactory
from utilities import handy_utilities
from utilities.util import Util
import time
import sys
from utilities.logger import customlogger
hand_util = handy_utilities.HandyUtilities()
log = customlogger(loglevel='DEBUG')

@pytest.yield_fixture(scope="session")
def setUp():
    print("Running method level setUp")
    util = Util()
    locator_list = util.get_locators_list()
    if locator_list is None:
        sys.exit()
    else:
        print('locators = ' + ":" + str([locator_list]))
    yield str(locator_list)
    print("Running method level tearDown")


@pytest.yield_fixture(scope="class")
def oneTimeSetUp(request, browser, url):
    print("Running one time setUp")
    log.info("#############" + __name__ + " ", hand_util.get_locator('Environment_variables', 'OS'))
    log.info("#############" + __name__ + " ", hand_util.get_locator('Environment_variables', 'OS_version'))
    wdf = WebDriverFactory(browser, url, 'browsers', 'Chrome', 'urls', 'practice_url')
    driver = wdf.getWebDriverInstance()
    util = Util()
    if request.cls is not None:
        request.cls.driver = driver

    yield driver
    time.sleep(2)
    driver.quit()
    util.move_files_to_directory('*.log')
    util.move_files_to_directory('*.html')
    print("Running one time tearDown")

def pytest_addoption(parser):
    parser.addoption("--browser")
    parser.addoption("--osType", help="Type of operating system")
    parser.addoption("--url")

@pytest.fixture(scope="session")
def browser(request):
    return request.config.getoption("--browser")

@pytest.fixture(scope="session")
def url(request):
    return request.config.getoption("--url")

@pytest.fixture(scope="session")
def osType(request):
    return request.config.getoption("--osType")
Driver Instance
There is a class with method which will return a driver instance based on the browser parameter passed to the oneTimeSetUp fixture so as soon as any class is initialized, oneTimeSetUp fixture will be executed to get the driver instance.
from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.firefox.options import Options as FirefoxOptions
from utilities.handy_utilities import HandyUtilities
from utilities.util import Util
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

class WebDriverFactory():

    def __init__(self, browser, url, *args):

        self.browser = browser
        self.baseURL = url
        self.handy_utilities = HandyUtilities()
        self.util = Util()
        for arg in args:
            if arg == 'browsers':
                self.key_browser = arg
            if arg in ['Chrome', 'Firefox', 'Safari']:
                self.data_browser = arg
            if arg == 'urls':
                self.key_url = arg
            if arg == 'practice_url':
                self.data_url = arg

        self.chrome_options = ChromeOptions()
        self.firefox_options = FirefoxOptions()

        self.firefox_capabilities = DesiredCapabilities.FIREFOX.copy()
        self.chrome_capabilities = DesiredCapabilities.CHROME.copy()
        self.chrome_capabilities['platform'] = 'macOS'        self.chrome_capabilities['version'] = "10.15.3"
    def getWebDriverInstance(self):

        if self.browser == "iexplorer":
            driver = webdriver.Ie()
        elif self.browser == "firefox":
            driver = webdriver.Firefox(firefox_options=self.firefox_options, desired_capabilities=self.firefox_capabilities)
        elif self.browser == "chrome":
            driver = webdriver.Chrome(chrome_options=self.chrome_options, desired_capabilities=self.chrome_capabilities)
        else:
            self.browser = self.util.get_locator(key=self.key_browser, data=self.data_browser)
            if self.browser == 'Chrome':
                driver = webdriver.Chrome(chrome_options=self.chrome_options, desired_capabilities=self.chrome_capabilities)
            elif self.browser == 'Firefox':
                driver = webdriver.Firefox(firefox_options=self.firefox_options, desired_capabilities=self.firefox_capabilities)
            else:
                raise Exception("Browser is not defined in config file")
        driver.implicitly_wait(3)
        driver.maximize_window()
        if self.baseURL is not None:
            driver.get(self.baseURL)
        else:
            url = self.util.get_locator(key=self.key_url, data=self.data_url)
            driver.get(url)
        return driver



when there are no parameters, arguments passed in the oneTimeSetUp fixture in conftest.py file, 
values/data will be fetched from the config.yaml file and it will be supplied to driver instance class 
above to get the required driver instance.


Config.yaml
Config.yaml file has all the configurations defined in it like environment variables, locators, paths, etc.
These variables and locators will be fetched/accessed by the test scripts. You just need to supply the location/path of the config.yaml file to the method/function which reads this file and gets the data.
# config.yaml file# ********************Environment variables************************
Environment_variables :
  OS: 'macOS Catalina'  OS_version: 'Version 10.15.3'
  project_path: '/Users/pravin.a/my-project/venv/MyFramework'  Driver_pth: '/usr/local/bin'
browsers:
  - Chrome
  - Firefox
  - Ie
  - Safari

urls: {
  practice_url: 'https://learn.letskodeit.com/p/practice',
  login_url: 'https://letskodeit.teachable.com/'}

credentials: {
  username: username,
  password: password,
  invalid_username: test@email.com,
  invalid_password: abcabc
}

#*****************************************Login Page/ Login Test Class Locators*************************
locators: {
  login_link: Login,
  email_field: "//input[@id='user_email']",
  password_field: "//input[@id='user_password']",
  login_button: "commit"}

#*****************************************Login Page/ Login Test Class Locators*************************


There is a method created to read the data from this config.yaml file and it will return a required data.
class HandyUtilities():
CONFIG_PATH = '<path where config.yaml file is located>'
def read_yaml(self, key, data):
    list_items = []
    with open(self.CONFIG_PATH) as file:
        list_items = yaml.load(file, Loader=yaml.Loader)
        try:
            for k, v in list_items.items():
                if k == key:
                    valt = list_items[key]
                    if type(valt) is dict:
                        for k, v in valt.items():
                            if k == data:
                                val = valt[data]
                                return val
                    elif type(valt) is list:
                        if data in valt:
                            print("data '{}' is found in config file".format(data))
                            return data
                        else:
                            raise Exception("data '{}' is not available in config file".format(data))
                else:
                    print("<Incorrect key>")
                    sys.exit()
        except:
            print(
                "Something is invalid in key/value pair '{}: {}' as this type of data is not supported by config  file".format(
                    key, data))
            return ('<Incorrect data found>')
Logs
In a framework it is required to capture the logs of all the activities which will be referred while troubleshooting any issue.
Custime logger method shown below is derived with the help of logging module which is in-built in pyhton.
import logging
import logging.config
import inspect
from datetime import datetime

def customlogger(loglevel=logging.DEBUG):
    # To get the name of the class/method from where this method is called.    now = datetime.now()
    loggerName = inspect.stack()[1][3]
    logger = logging.getLogger(loggerName)

    # Print all log message    logger.setLevel(logging.DEBUG)

    fileHandler = logging.FileHandler('{0}'.format(loggerName)+'_'+str(now.strftime("%Y%m%d%H%M%S %p")) +
                       '.log', mode='w')
    fileHandler.setLevel(loglevel)

    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s',
                    datefmt='%m/%d/%Y %I:%M:%S %p')

    fileHandler.setFormatter(formatter)

    logger.addHandler(fileHandler)

    return logger


This customer logger method returns a logger which is used to print a log. Object will be created of this method and logs will be printed as shown below
log = customlogger(loglevel=logging.DEBUG)
log.info("Message")
log.error("Message")
log.warn("Message")
Running tests
'pytest' and 'unittest' frameworks are used to make framework more efficient and effective. Run the test scripts using pytest and generate html report. (venv) pravin-a:SampleProject pravin.a$ pytest --html=Result.html
pytest will look for all the scripts that start with 'test' keyword and execute it. As shown above, it will give you the details like what all tests are run and their status.
The HTML report is generated at the current directory location.
Log files
logs will be generated by the framework and it will be stored in the files created by custom logger method. Sample extract from the log file is shown below

Running tests in TestSuite
'Unittest' framework of Python provides great features and methods to collect multiple test methods/cases from different-different test classes and bundle them together in a test suite.
import unittest
from tests.login.test_login import LoginTests
# Pull the test cases/methods from the test class

Login_TCs = unittest.TestLoader().loadTestsFromTestCase(LoginTests)

# Create a test suite and add the pulled test cases to it

smoke_suite = unittest.TestSuite(Login_TCs)

#Run the test suite

unittest.TextTestRunner(verbosity=2).run(smoke_suite)



Behavior Driven Development (BDD)



The most effective and stable framework for BDD in Python is 'behave'
You just need to install behave using pip3 (If you are using Python3)
--> pip3 install behave
As stated above in point #9 in 'How to build an automation framework' section, BDD related stuff i.e. feature file, steps declaration, and actual code files are typically organized in below hierarchy.
Feature file : login.feature
feature file will contain the features you want to test e.g. in this case feature is 'Verify login functionality' or it could be just 'login'
Scenario: Scenarios pertaining to feature Login e.g. login with valid credentials, login with invalid credentials.
@given --> This is nothing but a pre-requisite to run your test.
@When --> Any action performed on UI.
@Then --> End result to verify after any action is performed.

Feature: Verify login functionality
  Scenario: Login with invalid credentials
    Given The user is on login page
    When User enters an invalid username
    And User enters an invalid password
    And clicks on login button
    Then Login is unsuccessful

  Scenario: Verify hide and show button functionality
    Given User is on practice home page
    When user clicks on Hide button
    Then text box will be hidden
    When user clicks on Show button
    Then text box will be visible

  Scenario: Verify mouse hover functionality
    Given User is on Practice home page of letskodeit
    When User mouse hover an element
    Then mouse hover action is completed
feature file
Steps declaration: steps.py
from behave import *
from BDD.login import TestDriver
from BDD.HideShow import testHideShow
from BDD.MouseHover import MouseHover

# This is a pre-requisite to access login page
@given('The user is on login page')
def step_impl(context):
    context.driver = TestDriver()
    context.driver.test_launchBrowser('chrome')

# This is a pre-requisite where user enters username
@when('User enters invalid username')
def step_impl(context):
    context.driver.test_uname('letskodeit@gmail.com')

# This is a pre-requisite where user enters password
@when('User enters invalid password')
def step_impl(context):
    context.driver.test_pwd('admin')

# This is a pre-requisite where user clicks on login button
@when('clicks on login button')
def step_impl(context):
    context.driver.test_click()

@then('Login is unsuccessful')
def step_impl(context):
    context.driver.test_unsuccessful()

#This is another scenario
@given('User is on practice home page')
def step_impl(context):
    context.obj = testHideShow()
    context.obj.test_HomePage()

@when('user clicks on Hide button')
def step_impl(context):
    context.obj.test_ActHide()

@then('text box will be hidden')
def step_impl(context):
    context.obj.test_HideBtn()

@when('user clicks on Show button')
def step_impl(context):
    context.obj.test_ActShow()

@then('text box will be visible')
def step_impl(context):
    context.obj.test_ShowBtn()

# ****** This is mouse hove scenario ******
@given('User is on Practice home page of letskodeit')
def step_impl(context):
    context.driver = MouseHover()
    context.driver.test_homePage()

@when('User mouse hover an element')
def step_impl(context):
    context.driver.test_mouseHover()

@then('mouse hover action is completed')
def step_impl(context):
    context.driver.test_complete()
Step declaration above contains a method that is nothing but an implementation of your actual test script for each statement defined in your feature file.
E.g. 
@given('The user is on login page')
def step_impl(context):
    context.driver = TestDriver()
    context.driver.test_launchBrowser('chrome')
Code above shows for @given statement from feature file 'The user is on login page', test method 'test_launchBrowser' is defined in step_impl method here which has all the code required to launch a browser shown below.
Method 'test_launchBrowser':
 def test_launchBrowser(self, browserType):
        if browserType=='chrome':
            self.driver.maximize_window()
            self.driver.get('url')
Note: Just to show you how the actual test method looks like, I have not copied the actual URL above and just referred it as 'url' in self.driver.get('url'). You need to pass actual url (https://*****.com)
Above code can be run using behave command along with the path as an argument where you feature file resides
(venv) pravin-a:Selenium_Projects pravin.a$ behave BDD/features/
BDD run
(venv) pravin-a:Selenium_Projects pravin.a$ behave BDD/features/
Feature: Verify login functionality # BDD/features/login.feature:1

  Scenario: Login with invalid credentials  # BDD/features/login.feature:2
    Given The user is on login page         # BDD/features/steps/steps.py:7 5.906s
    When User enters an invalid username    # BDD/features/steps/steps.py:13 2.188s
    And User enters an invalid password     # BDD/features/steps/steps.py:18 2.076s
    And clicks on login button              # BDD/features/steps/steps.py:23 3.123s
    Then Login is unsuccessful              # BDD/features/steps/steps.py:27 0.163s

  Scenario: Verify hide and show button functionality  # BDD/features/login.feature:9
    Given User is on practice home page                # BDD/features/steps/steps.py:32 6.472s
    When user clicks on Hide button                    # BDD/features/steps/steps.py:37 4.422s
    Then text box will be hidden                       # BDD/features/steps/steps.py:41 0.000s
    When user clicks on Show button                    # BDD/features/steps/steps.py:45 0.465s
    Then text box will be visible                      # BDD/features/steps/steps.py:49 0.000s

  Scenario: Verify mouse hover functionality          # BDD/features/login.feature:16
    Given User is on Practice home page of letskodeit # BDD/features/steps/steps.py:54 0.001s
    When User mouse hover an element                  # BDD/features/steps/steps.py:59 8.105s
    Then mouse hover action is completed              # BDD/features/steps/steps.py:63 0.000s

1 feature passed, 0 failed, 0 skipped
3 scenarios passed, 0 failed, 0 skipped
13 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m32.920s
(venv) pravin-a:Selenium_Projects pravin.a$ 
As you can see above it will give you all the information after once all the scenarios are executed successfully.
E.g. How many features are executed/passed, how many scenarios in total are executed and passed/failed/skipped and the total no. of steps executed/passed/skipped.
You can also generate a report called 'allure' report for your test run using below command
(venv) pravin-a:Selenium_Projects pravin.a$ behave -f allure_behave.formatter:AllureFormatter -o %allure_result_folder% BDD/features
Allure report generation
View the generated allure-report
allure report
Put It All Together
To put it together, you can make use of this hybrid automation test framework to automate UI's of your application and you can customise this framework as per your need.

Comments

Post a Comment