from selenium import webdriverUI AutomationUI 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.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.
- Unittest
- Pytest
- Page object model
- Data driven testing
- 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:
- 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 classThe 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.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 ClassNow 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 customloggerhand_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 driverwhen 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.yamlConfig.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>')LogsIn 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 loggerThis 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.htmlpytest 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 fileslogs 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 unittestfrom 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 itsmoke_suite = unittest.TestSuite(Login_TCs) #Run the test suiteunittest.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 behaveAs 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.featurefeature 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 completedSteps declaration: steps.pyfrom 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/(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/featuresView the generated allure-reportPut It All TogetherTo 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.
you have written an excellent blog.. keep sharing your knowledge...
ReplyDeleteSelenium with Python Online Training
Python Automation Testing Course
ReplyDeleteWonderful post and more informative!keep sharing Like this!
Ethical Hacking Course in Bangalore
Ethical Hacking Course in Pune
Ethical Hacking Course in Hyderabad
Ethical Hacking Course in Delhi
Ethical Hacking Course in Gurgaon
Really good blog and very helpful
ReplyDelete