Creación de un simple POM con Python – Behave
Continuando con la serie que comenzamos hace un par de semanas sobre cómo hacer un POM simple con Java, hoy quiero dejaros otra forma simple de generar un POM con Python en Behave, para mi gusto un gran framework al que se le puede sacar muchísimo rendimiento.
Como siempre quiero mencionar que son pequeños manuales para que las personas que están empezando en este apasionante mundillo sepan y entiendan como generar sus propios POM y por qué la necesidad de los mismos que no es otra que tener todo organizado De la mejor forma posible para entender, mantener y reutilizar el código, con una base estable de pruebas.
Como vimos el otro día las capas básicas que forman un POM, son las siguientes:
- features, (cucumber scenarios & steps), hablaremos más en detalle en otro post (como principal objetivo es documentar y probar funcionalidades por separado), además también tendrá los steps comunes o genéricos y el fichero environment, el cual contendrá el método before_scenario y after_scenario
- pages, una por cada página de la web
- web_source, estarán los métodos comunes y la instancia al chormedriver
- drivers, estarán los diferentes drivers para poder trabajar con nuestro navegador
NOTA: para los que vieron mi post anterior este diagrama difiere un poco del otro, pero al final la esencia es la misma
Documentación:
- bahave: https://behave.readthedocs.io/en/stable
- cucumber: https://cucumber.io/
- selenium: https://www.selenium.dev/documentation/es/
- behave-allure: https://pypi.org/project/allure-behave/
Instalación:
Nota: probado sobre versión 3.8.5 de python
- pip3 install behave
- pip3 install behave2cucumber
- pip3 install cucumber-tag-expressions
- pip3 install behave-html-formatter
- pip3 install behave-jenkins
- pip3 install pylint
- pip3 install allure-behave
- npm install -g allure-commandline –save-dev
Con todo esto ya instalado y sin mayor dilación manos a la obra y en honor al gran “Martín Berasategui” vamos a darle “garrote”:
Nota: como siempre digo es un entorno simple para poder comenzar hacer pruebas, siempre abierto a mejoras y modificaciones oportunas necesarias.
web_source:
web_factory.py: Contendrá el método con path al driver a usar, si quisiéramos poner por ejemplo geckodriver, únicamente deberíamos añadir la condicional e igualarla a Firefox por ejemplo donde dentro tendríamos nuestra ruta al mismo.
from selenium import webdriver from web_source.web import Web def get_web(browser): if browser == "chrome": return Web(webdriver.Chrome("drivers/chromedriver.exe"))
web.py: la clase Web, tendrá todos los métodos necesarios del WebDriver a usar en nuestra implementación.
from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class Web(object): __TIMEOUT = 10 def __init__(self, web_driver): super(Web, self).__init__() self._web_driver_wait = WebDriverWait(web_driver, Web.__TIMEOUT) self._web_driver = web_driver def open(self, url): self._web_driver.get(url) def maximize(self): self._web_driver.maximize_window() def title(self): return self._web_driver.title def curent_url(self): return self._web_driver.current_url def get_text_xpath(self, xpath): return self._web_driver_wait.until(EC.presence_of_element_located((By.XPATH, xpath))).text def find_by_xpath(self, xpath): return self._web_driver_wait.until(EC.visibility_of_element_located((By.XPATH, xpath))) def finds_by_xpath(self, xpath): return self._web_driver_wait.until(EC.presence_of_all_elements_located((By.XPATH, xpath))) def find_by_xpath_displayed(self, xpath): return self._web_driver_wait.until(EC.visibility_of_element_located((By.XPATH, xpath))).is_displayed() def find_by_name(self, name): return self._web_driver_wait.until(EC.visibility_of_element_located((By.NAME, name))) def finds_by_name(self, name): return self._web_driver_wait.until(EC.presence_of_all_elements_located((By.NAME, name))) def find_by_id(self, id): return self._web_driver_wait.until(EC.visibility_of_element_located((By.ID, id))) def find_by_id_displayed(self, id_value): return self._web_driver_wait.until(EC.presence_of_element_located((By.ID, id_value))).is_displayed() def finds_by_id(self, id): return self._web_driver_wait.until(EC.presence_of_all_elements_located((By.ID, id))) def find_by_class_name(self, classname): return self._web_driver_wait.until(EC.visibility_of_element_located((By.CLASS_NAME, classname))) def finds_by_class_nam(self, classname): return self._web_driver_wait.until(EC.presence_of_all_elements_located((By.CLASS_NAME, classname))) def find_by_css_selector(self, cssselector): return self._web_driver_wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, cssselector))) def finds_by_css_selector(self, cssselector): return self._web_driver_wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, cssselector))) def switch_frame(self,frame): return self._web_driver.switch_to.frame(frame) def close(self): self._web_driver.quit()
page_object:
- dashboard_orange_hrm.py: la clase DashboardOrangeHrm, contiene métodos necesarios de la página que queremos probar
from page_object.orangehrm.locators_orange_hrm import LocatorsHomeOrangeHrm class DashboardOrangeHrm(LocatorsHomeOrangeHrm): def __init__(self): self.locators = self.locators_dashboard def dashboard_header(self, context): text = context.web.get_text_xpath(self.locators["xpath_dashboard_header"]) if text == self.locators["Dashboard_header_name"]: return True return False
- home_orange_hrm.py: Esta clase HomeOrangeHrm al igual que la anterior debe contener los métodos necesarios de la página a testear.
from page_object.orangehrm.locators_orange_hrm import LocatorsHomeOrangeHrm class HomeOrangeHrm(LocatorsHomeOrangeHrm): def __init__(self): self.locator = self.locators def simple_login_home(self, context, user, pwd): user_name = context.web.find_by_id_displayed(self.locators["id_username"]) password = context.web.find_by_id_displayed(self.locators["id_password"]) if user_name is True and password is True: context.web.find_by_id(self.locators["id_username"]).send_keys(user) context.web.find_by_id(self.locators["id_password"]).send_keys(pwd) return True return False def table_login_home(self, context, name, pwd): for row in context.table: context.temp_name = row['name'] context.temp_password = row['pwd'] user_name = context.web.find_by_id_displayed(self.locators["id_username"]) password = context.web.find_by_id_displayed(self.locators["id_password"]) if user_name is True and password is True: context.web.find_by_id(self.locators["id_username"]).send_keys(context.temp_name) context.web.find_by_id(self.locators["id_password"]).send_keys(context.temp_password) return True return False def example_login_home(self, context, user, pwd): context.temp_name = user context.temp_password = pwd user_name = context.web.find_by_id_displayed(self.locators["id_username"]) password = context.web.find_by_id_displayed(self.locators["id_password"]) if user_name is True and password is True: context.web.find_by_id(self.locators["id_username"]).send_keys(context.temp_name) context.web.find_by_id(self.locators["id_password"]).send_keys(context.temp_password) return True return False def invalid_login(self, context): message = context.web.get_text_xpath(self.locators["xpath_message_error"]) if message == "Invalid credentials": return True return False def view_logo(self, context): logo = context.web.find_by_xpath_displayed(self.locators["xpath_logo"]) return logo def submit_bottom(self, context): context.web.find_by_id(self.locators["id_btn_login"]).click()
- locator_orange_hrm.py: hemos escogido esta forma de datafactory para almacenar lo locators, pues nos ha parecido para este ejemplo la más didáctica, hay muchas otras pero esta también es una buena forma de tener los locators controlados, por lo tanto la clase locator se usará para almacenar los localizadores a usar en nuestra implementación.
class LocatorsHomeOrangeHrm: locators = { "id_username": "txtUsername", "id_password": "txtPassword", "xpath_message_error": "//span[@id='spanMessage']", "xpath_logo": "//div[@id='divLogo']//img", "id_btn_login": "btnLogin", "credential_error": "Invalid credentials" } locators_dashboard = { "xpath_dashboard_header": "//h1[contains(text(),'Dashboard')]", "Dashboard_header_name": "Dashboard" }
features:
- orange_hrm_home.feature: En este fichero de tipo .feature, tendremos todos los scenarios necesarios para nuestras pruebas, tendremos tantos como necesitemos, una buena recomendación es ordenarlos por página y tipo de pruebas (ej. Casos negativos, casos positivos) por funcionalidad.
Nota: como comentamos antes haremos un post sobre gerking, pero en las líneas a continuación ya podéis observar dos tipos de escenarios diferente, por un lado el escenario normal (llamado Scenario), y por otro lado los escenarios outlines (llamados Scenarios Outline), así mismo podéis ir viendo algunas diferencias entre ambos
Feature: OrangeHRM outline @test Scenario: Login to OrangeHRM with valid parameters Given the user is on the search page "orangehrmlive" When Enter username "admin" and password "admin123" And press on login button Then User must succesfully login to the Dashboard page @test Scenario: Logo presence on OrangeHRM home page Given the user is on the search page "orangehrmlive" When the user verify the title page "OrangeHRM" Then verify that the logo present on page @test Scenario Outline: Login table Examples valid to OrangeHRM Given the user is on the search page "orangehrmlive" When insert username "<name>" and password "<pwd>" And press on login button Then User must succesfully login to the Dashboard page Examples: |name | pwd | |admin | admin123| @test Scenario Outline: Login table Examples invalid to OrangeHRM Given the user is on the search page "orangehrmlive" When insert username "<name>" and password "<pwd>" And press on login button Then User must unsuccesfully login to the Dashboard page Examples: |name | pwd | |admin | admin1 | |admin1 | admin123 | |admin | admin |
- orange_hrm_dashboard.feature: Escenario de la prueba que realizamos sobre la página llamada dashboard, que no es otra más que el login de entrada, como os imagináis de un login básico como este se pueden sacar como mínimo 4 casos negativos y uno positivo, los cuales os los dejo para que los penséis.
Nota: el positivo ya está en el ejemplo.
Feature: OrangeHRM Dashboard @test Scenario: Login table to OrangeHRM Given the user is on the search page "orangehrmlive" When put username "<name>" and password "<pwd>" |name | pwd | |admin| admin123| And press on login button Then User must succesfully login to the Dashboard page
Steps:
- orange_hrm_steps.py: Este fichero contendrá cada uno de os steps definidos en cada uno de los escenarios de los features, cada vez que llegue a un step (Given, When, Then, And), buscará si existe el mismo y lo ejecutará en caso contrario fallara y nos mostrará el step que deberemos implementar.
from behave import when, then from page_object.orangehrm.home_orange_hrm import HomeOrangeHrm from page_object.orangehrm.dashboard_orange_hrm import DashboardOrangeHrm global HOME @when('the user verify the title page "{titlepage}"') def title_page(context, titlepage): title = context.web.title() assert title == titlepage @then('verify that the logo present on page') def verify_logo(context): home = HomeOrangeHrm() result = home.view_logo(context) assert result is True # Login @when('Enter username "{user}" and password "{pwd}"') def login(context, user, pwd): home = HomeOrangeHrm() result = home.simple_login_home(context, user, pwd) assert result is True # login table with header @when('put username "{name}" and password "{pwd}"') def login_table(context, name, pwd): home = HomeOrangeHrm() result = home.table_login_home(context, name, pwd) assert result is True # login outline valid @when('insert username "{user}" and password "{pwd}"') def login_outline(context, user, pwd): home = HomeOrangeHrm() result = home.example_login_home(context, user, pwd) assert result is True # finish valid login @then('User must succesfully login to the Dashboard page') def dashboard(context): home = DashboardOrangeHrm() dashboard_head = home.dashboard_header(context) assert dashboard_head is True # finish invalid login @then('User must unsuccesfully login to the Dashboard page') def login_error(context): home = HomeOrangeHrm() result = home.invalid_login(context) assert result is True
- hook: Este fichero contendrá los steps genéricos o comunes para todas las pruebas de esta forma evitaremos repeticiones de código.
from behave import given, when from page_object.orangehrm.home_orange_hrm import HomeOrangeHrm from page_object.telefonica.google_telefonica import google_telefonica @given('the user is on the search page "{page}"') def user_on_search_page(context, page): if page == "orangehrmlive": context.web.open('http://opensource-demo.orangehrmlive.com/') context.web.maximize() bing.accept_cookies(context) @when('Click on login button') def submit_login_form(context): submit = HomeOrangeHrm() submit.submit_bottom(context)
Environment:
Tendremos en el mismo los métodos “before_scenario” y “after_scenario”, los cuales se ejecutar antes y después de cada escenario.
- before_scenario: Básicamente lo que hacemos es pasarle al método get_web del web_factory anteriormente mencionado el browser a usar, como nosotros únicamente tenemos para este ejemplo configurado el Chrome, pues le estamos pasando el valor de chrome, para que así cargue el driver correspondiente al Chrome.
- After_scenario: En este lo que hacemos es cerrar la instancia del browser
from web_source.web_factory import get_web def before_scenario(context, test): web = get_web("chrome") context.web = web def after_scenario(context, test): context.web.close()
Por último y dado que behave nos dice que debemos tener todo sobre el mismo directorio feaures y nosotros creemos que es mejor tenerlo separado, debemos de generar el siguiente fichero a la altura del hook.
All_steps:
Nota: en el mismo hay que indicarle la ruta a los steps, de esa manera podríamos tenerlos divididos por directorios diferentes, solo añadiendo en este las rutas a los mismos.
from features.steps.orange_hrm import *
I want be chamaleon, gracias a todo el equipo.