Introduction to page object design pattern of selenium automation design framework

Table of contents

Introduction to PageObjects

PageObject uses

The Six Principles of PageObject

Practical case of PO based on Dingding check-in

Actual code

Summarize:


The pageobject design idea comes from an article on the official website of Martin Flower (yes, yes, the godfather of software), the official website link: https://martinfowler.com/bliki/PageObject.html There is also an article
for the official website The Chinese translation version: http://huangbowen.net/blog/2013/09/17/page-object
So, if your English is not very good, you may wish to read the Chinese version, the article is very detailed

Introduction to PageObjects

When writing test cases for UI pages (such as web pages, mobile pages), there will be a large number of elements and operational details in the test cases. How to deal with the problem that when the UI changes, the test cases should also change accordingly? The PageObject design pattern makes its debut (proposed by IT guru Martin Flower).

When using UI automation testing tools (Selenium, Appium, etc.), if there is no unified model for specification, it will become difficult to maintain as the number of use cases increases, and PageObject makes automation scripts orderly, maintains pages separately and encapsulates details, which can be To make the testcase more robust, no major changes are required.

PageObject uses

Specific method: Encapsulate the element information and operation details into the Page class, and call the Page object (PageObject) on the test case. For example, there is a function "select album title", and a function selectAblumWithTitle() needs to be created for it. Inside the function is the operation details findElementsWithClass('album') etc:


Taking "Get Album Title" as an example, the pseudocode is as follows: 

selectAblumWithTitle() {    #选取相册    findElementsWithClass('album')    #选取相册标题    findElementsWithClass('title-field')    #返回标题内容    return getText() }

The main principle of PageObject is to provide a simple interface (or function, such as the above-mentioned selectAblumWithTitle ), so that the caller can do any operation on the page, click on page elements, enter content in the input box, etc. So, if you want to access a text field, the Page Object should have methods for getting and returning strings. The Page Object should encapsulate the details of manipulation of the data, such as finding elements and clicking elements. When the page elements are changed, only the content in the Page class should be changed, and there is no need to change the place where it is called.

Instead of creating a page class for every UI page, create page classes only for the important elements of the page. For example, if a page displays multiple albums, an album list page object should be created, which contains many album page objects. If some complex UI hierarchy is only used to organize the UI, then it should not appear in the page object. The purpose of the page object is to make sense to the consumer of the application by modeling the page:

 
If you want to navigate to another page, the initial page object should return another page object, such as clicking register to enter the registration page, and return Register() in the code. If you want to get page information, you can return basic types (string, date).

It is recommended not to put assertions in the page object. The page object should be tested instead of allowing the page object to test itself. The responsibility of the page object is to provide page status information. Here only HTML is used to describe Page Object, this mode can also be used to hide Java swing UI details, it can be used in all UI frameworks.

The Six Principles of PageObject

Selenium has condensed six principles for the core idea of ​​PageObject. Only by mastering the essence of the six principles can we conduct PageObject best practice drills:

  • Public methods represent services provided by the page
  • Do not expose page details
  • Don't mix assertions with operational details
  • The method can return to the newly opened page
  • Don't put whole page content in PO
  • The same behavior will produce different results, different results can be encapsulated

Below, a more detailed practical explanation of the above six principles:
Principle 1: To encapsulate the functions (or services) in the page, such as clicking on an element in the page, you can enter a new page, so you can encapsulate this service method "go to new page".
Principle 2: Encapsulate details, and only provide method names (or interfaces) to the outside world.
Principle 3: Do not use assertions in the encapsulated operation details, put the assertions in a separate module, such as testcase.
Principle 4: Clicking a button will open a new page, and the return method can be used to indicate a jump, such as return MainPage() means to jump to a new PO: MainPage.
Principle 5: Only do PO design for important elements on the page, and discard unimportant content.
Principle 6: An action may produce different results. For example, after clicking a button, the click may be successful or the click may fail. There are two results

Practical case of PO based on Dingding check-in


Use the Page Object principle to model the page, which involves three pages: login page, home page, workbench page, and attendance check-in page. Create the corresponding three classes in the code LoginPage, MainPage, WorkBenchPage, AttendancePage, (this demonstration involves less function points, the required functions are all encapsulated in MainPage)

Actual code

Directory Structure


BasePage is the parent class of all page objects, and it provides public methods for subclasses. For example, the following BasePage provides initialization driver and exit driver. Use, driver assignment, global wait setting (implicit wait), etc.:

#!/usr/bin/python# -*- coding: UTF-8 -*-"""@author:chenshifeng@file:base_page.py@time:2020/11/19@功能:实现钉钉自动打卡""" import loggingimport osimport timeimport yamlfrom appium.webdriver import WebElementfrom selenium.webdriver.remote.webdriver import WebDriverfrom selenium.webdriver.support import expected_conditionsfrom selenium.webdriver.support.wait import WebDriverWait file_name = 'case_' + time.strftime("%Y_%m_%d", time.localtime()) + '.log'home_dir = (os.path.dirname(os.path.dirname(os.path.abspath(__file__))))log_file = os.path.join(home_dir, 'log', file_name)  class BasePage:    root_logger = logging.getLogger()    print(f"root_logger.handlers:{root_logger.handlers}")    for h in root_logger.handlers[:]:        root_logger.removeHandler(h)    logging.basicConfig(format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s',                        level=logging.INFO, filename=log_file, filemode='a')     # logging.StreamHandler(sys.stderr).setLevel(logging.INFO)     def __init__(self, driver: WebDriver = None):        self.driver = driver     def find(self, by, locator=None, timeout=20) -> WebElement:        '''        查找元素        :param by:        :param locator:        :return:        '''        logging.info(f'find.by:{by}')        logging.info(f'find.locator:{locator}')        if locator is None:            result = WebDriverWait(self.driver, timeout).until(expected_conditions.element_to_be_clickable(*by))        else:            result = WebDriverWait(self.driver, timeout).until(                expected_conditions.element_to_be_clickable((by, locator)))        logging.info(f'查找结果:{result}')        return result     def parse_yaml(self, path, func_name):        logging.info(f'parse_yaml.path:{path}')        logging.info(f'parse_yaml.func_name:{func_name}')        with open(path, encoding='UTF-8') as f:            datas = yaml.safe_load(f)            self.parse(datas[func_name])     def parse(self, steps):        for step in steps:            if 'clear' in step['action']:                self.find(step['by'], step['loactor']).clear()            if 'click' in step['action']:                self.find(step['by'], step['loactor']).click()            if 'send_keys' in step['action']:                self.find(step['by'], step['loactor']).send_keys(step['context'])     def get_pagesource(self):        return self.driver.page_source     def get_screenshot(self, path):        return self.driver.save_screenshot(path)     def assert_result(self, element, timeout=20):        result = WebDriverWait(self.driver, timeout).until(lambda x: element in x.page_source)    # 解决手机卡顿造成的误判        logging.info(f'断言结果:{result}')        return result

Since DingTalk is a mobile APP, I wrote a separate APP class, which includes functions such as login, stop, restart, and close the APP.

#!/usr/bin/python# -*- coding: UTF-8 -*-"""@author:chenshifeng@file:app.py@time:2020/11/19 app.py 模块,存放app相关的一些操作。比如 启动应用,重启应用,停止应用,登录App,进入到首页"""import osimport yamlfrom appium import webdriver from dingding.page.base_page import BasePagefrom dingding.page.main_page import MainPage home_dir = (os.path.dirname(os.path.dirname(os.path.abspath(__file__))))appconfig_file = os.path.join(home_dir,'config','appconfig.yml') with open(appconfig_file,encoding='UTF-8') as f:    datas = yaml.safe_load(f)    desired_caps = datas['desired_caps']    ip = datas['ip']  class APP(BasePage):     def start(self):        if self.driver is None:            self.driver = webdriver.Remote(f'http://{ip}/wd/hub', desired_caps)            self.driver.implicitly_wait(5)        else:            self.driver.launch_app()        return self     def login(self):        MainPage(self.driver).login()     def restart(self):        pass     def stop(self):        pass     def close(self):        pass     def goto_main_page(self):        return MainPage(self.driver)

LoginPage is the page object of the DingTalk login page. It has two methods, entering the registration page object and entering the login page object. Here, the return method returns the page object to realize the page jump. For example: the goto_register method returns Register to realize the jump from the home page to On the registration page, the login in the demo code is written in the APP class. This can be packaged according to personal habits. There is no fixed template
. Fewer functions, uniformly encapsulated into the MainPage class

#!\usr\bin\python# -*- coding: UTF-8 -*-"""@author:chenshifeng@file:main_page.py@time:2020\11\19@tip:由于我们只需部分功能,故本次把所有所需功能都放入本页面内,不做多个页面的封装"""import os from dingding.page.base_page import BasePage home_dir = (os.path.dirname(os.path.dirname(os.path.abspath(__file__))))main_page_file = os.path.join(home_dir, 'config', 'main_page.yml')  class MainPage(BasePage):    def login(self):        self.parse_yaml(main_page_file, 'login')     def goto_workbench(self):        self.parse_yaml(main_page_file, 'goto_workbench')        return self     def chose_company(self):        self.parse_yaml(main_page_file, 'chose_company')        return self     def goto_attendance(self):        self.parse_yaml(main_page_file, 'goto_attendance')        return self     def clock_in(self):        self.parse_yaml(main_page_file, 'clock_in')     def clock_out(self):        self.parse_yaml(main_page_file, 'clock_out')     def update_clock_out(self):        self.parse_yaml(main_page_file, 'update_clock_out')

The conftest.py file is unique to the pytest framework and is used to do some use case pre-operations

import osimport signalimport time import pytestimport subprocess  @pytest.fixture(scope='module', autouse=True)def case_before():     # 启动appium    file_name = 'appium_' + time.strftime("%Y_%m_%d", time.localtime()) + '.log'    home_dir = (os.path.dirname(os.path.dirname(os.path.abspath(__file__))))    log_file = os.path.join(home_dir, 'log', file_name)    command = f'appium -p 4723  --session-override --log-timestamp	--local-timezone >> {log_file}'    p_appium = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)    # yield    # p_appium.kill()     # 解锁手机    # true值为锁屏,false为解锁    mIsShowing = subprocess.getstatusoutput('adb shell dumpsys window policy | find "mIsShowing="')[1]    mIsShowing = mIsShowing.split('=')[1]    # OFF 黑屏  ON 亮屏    power = subprocess.getstatusoutput('adb shell dumpsys power | find "Display Power: state="')[1]    power = power.split('=')[1]    # 尝试解锁    # os.system('adb shell input keyevent 82')    if power == 'OFF':  # 判断是否黑屏(默认黑屏自动锁屏)        os.system('adb shell input keyevent 26')  # 亮屏        os.system('adb shell input swipe 540 2000 540 500 50')  # 滑动解锁    if power == 'ON' and mIsShowing == 'true':  # 判断是否锁屏        os.system('adb shell input swipe 540 2000 540 500 50')  # 滑动解锁     # # 录屏功能    # video_file_name = 'video_' + time.strftime("%Y_%m_%d", time.localtime()) + '.mp4'    # video_log_file = os.path.join(home_dir, 'video', video_file_name)    # command = f"scrcpy --record {video_log_file}"    # p_video = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)    # yield    # print(p_appium.pid)    # # p = subprocess.getstatusoutput(f'taskkill /PID  {p_appium.pid} /F')    # print(subprocess.getstatusoutput(f'taskkill /PID  {p_appium} /F'))

The test_gooffwork and test_gotowork modules are tests for the above functions, which are independent of the page class, and only need to call the method provided by the page class in the TestIndex class

#!/usr/bin/python# -*- coding: UTF-8 -*-"""@author:chenshifeng@file:test_gotowork.py@time:2020/11/19@case:钉钉上班打卡case"""import subprocess import pytest from dingding.page.app import APP  class TestGoToWork:    def setup(self):        self.app = APP()        self.main = self.app.start()        # 登录处理        try:            self.main.login()        except Exception:            pass         self.page = self.main.goto_main_page()     def teardown(self):        #     关闭appium        p_appium = subprocess.getstatusoutput('netstat -ano|findstr 4723|findstr LISTENING')[1]        # print(p_appium)        p_appium = p_appium.split(' ')[-1]        appium_kill = subprocess.getstatusoutput(f'taskkill /PID {p_appium} /F ')[0]        print(appium_kill)     @pytest.mark.flaky(reruns=3, reruns_delay=2)  # 失败重跑    def test_gotowork(self):        self.attendance_page = self.page.goto_workbench().chose_company().goto_attendance()        self.attendance_page.clock_in()        assert self.attendance_page.assert_result('打卡成功') 
#!/usr/bin/python# -*- coding: UTF-8 -*-"""@author:chenshifeng@file:test_gooffwork.py@time:2020/11/19@case:钉钉下班打卡case"""import subprocess import pytest from dingding.page.app import APP  class TestGoOffWork:     def setup(self):        self.app = APP()        self.main = self.app.start()        # 登录处理        try:            self.main.login()        except Exception:            pass         self.page = self.main.goto_main_page()     def teardown(self):        #     关闭appium        p_appium = subprocess.getstatusoutput('netstat -ano|findstr 4723|findstr LISTENING')[1]        print(p_appium)        p_appium = p_appium.split(' ')[-1]        appium_kill = subprocess.getstatusoutput(f'taskkill /PID {p_appium} /F ')[0]        print(appium_kill)     @pytest.mark.flaky(reruns=3, reruns_delay=2)  # 失败重跑    def test_gooffwork(self):        self.attendance_page = self.page.goto_workbench().chose_company().goto_attendance()         # 若打卡失败,进行更新打卡        try:            self.attendance_page.clock_out()        except Exception:            self.attendance_page.update_clock_out()         assert self.attendance_page.assert_result('打卡成功')

In addition, there is a configuration folder config to initialize the app and element positioning
appconfig.yml file with open configuration

desired_caps:  "platformName": "Android"  "automationName": "UiAutomator2"  "platformVersion": "10"  "deviceName": "test"  "appActivity": 'com.alibaba.android.rimet.biz.LaunchHomeActivity'  "appPackage": 'com.alibaba.android.rimet'#  "settings[waitForIdleTimeout]": 0  "noReset": True  # 不重置APP  "skipServerInstallation": True  # 跳过 uiAutomator2服务器安装#  "dontStopAppOnReset": True,  "skipDeviceInitialization": True   # 跳过设备初始化#  "unicodeKeyboard": True  # 默认启用 Unicode 输入#  "resetKeyboard": True  # 与上面合用,可以输入中文 ip:  127.0.0.1:4723

main_page.yml

login:# 登陆  - by : id    loactor: 'com.alibaba.android.rimet:id/et_phone_input'    action: clear,send_keys    context: '1865xxxx166'   - by: id    loactor: 'com.alibaba.android.rimet:id/et_pwd_login'    action: send_keys    context: '123456'   - by : id    loactor: 'com.alibaba.android.rimet:id/tv'    action: click goto_workbench:# 进入工作台  - by : xpath    loactor: '//*[@text="工作台"]'    action: click chose_company:# 选择企业  - by: id    loactor: 'com.alibaba.android.rimet:id/menu_current_company'    action: click   - by: xpath    loactor: '//*[@text="阿里外包钉钉考勤专用组"]'    action: click goto_attendance:# 进入考勤页  - by: -android uiautomator    loactor: 'new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("考勤打卡").instance(0));'    action: click clock_in:# 上班打卡  - by : xpath    loactor: '//*[@text="上班打卡"]'    action: click clock_out:# 下班打卡  - by : xpath    loactor: '//*[@text="下班打卡"]'    action: click update_clock_out:# 更新下班打卡  - by : xpath    loactor: '//*[@text="更新打卡"]'    action: click   - by: xpath    loactor: '//*[@text="确定"]'    action: click #cheack_result:## 更新下班打卡#  - by : xpath#    loactor: '//*[@text="更新打卡"]'#    action: click##  - by: xpath#    loactor: '//*[@text="确定"]'#    action: click

Summarize:

Thanks to everyone who read my article carefully! ! !

 I personally sorted out some technical materials I have compiled in my software testing career in the past few years, including: e-books, resume modules, various job templates, interview books, self-study projects, etc. Everyone is welcome to leave a message in the comment area 333 to get it for free, don't miss it.

Guess you like

Origin blog.csdn.net/MXB_1220/article/details/131712100