selenium自动化设计框架之 page object设计模式介绍

news2024/11/27 3:45:23

目录

PageObject 简介

PageObject 使用

PageObject 六大原则

基于钉钉打卡的 PO 实战案例

实战代码

总结:


pageobject设计思想出自于马丁·福勒(Martin Flower,对,没错,就是软件教父)官网的一篇文章,官网连接:https://martinfowler.com/bliki/PageObject.html
还有一篇针对官网文章的中文翻译版:http://huangbowen.net/blog/2013/09/17/page-object
所以,如果你的英文不是很好,不妨看下中文版的,文章讲解的非常详细

PageObject 简介

在为 UI 页面写测试用例时(比如 Web 页面,移动端页面),测试用例会存在大量元素和操作细节。如何面对当 UI 变化时,测试用例也要跟着变化这个问题?PageObject 设计模式闪亮登场(由 IT 大佬 Martin Flower 提出)。

使用 UI 自动化测试工具时(Selenium、Appium 等),如果无统一模式进行规范,随着用例的增多会变得难以维护,而 PageObject 让自动化脚本井井有序,将 page 单独维护并封装细节,可以使 testcase 更稳健,不需要大改动。

PageObject 使用

具体做法:把元素信息和操作细节封装到 Page 类中,在测试用例上调用 Page 对象(PageObject),比如存在一个功能“选取相册标题”,需要为之建立函数selectAblumWithTitle(),函数内部是操作细节findElementsWithClass('album')等:


以选“取相册标题”举例,伪代码如下: 

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

PageObject 的主要原则是提供一个简单接口 (或者函数,比如上述的 selectAblumWithTitle ),让调用者在页面上可以做任何操作,点击页面元素,在输入框输入内容等。因此,如果要访问一个文本字段,Page Object 应该有获取和返回字符串的方法。Page Object 应该封装对数据的操作细节,比如查找元素和点击元素。当页面元素改动时,应该只改变 Page 类中的内容,不需要改变调用它的地方。

不要为每个 UI 页面都创建一个 page 类,应该只为页面中重要的元素创建 page 类。比如,一个页面显示多个相册,应该创建一个相册列表 page object,它包含许多相册 page object。如果某些复杂 UI 的层次结构只是用来组织 UI,那么它就不应该出现在 page object 中。page object 的目的是通过给页面建模,从而对应用程序的使用者变得有意义:

 
如果你想导航到另一个页面,初始 page 对象应当 return 另一个 page 对象,比如点击注册,进入注册页面,在代码中就应该 return Register()。如果想获取页面信息,可以 return 基本类型(字符串、日期)。

建议不要在 page object 中放断言。应该去测 page object,而不是让 page object 自己测自己,page object 的责任是提供页面的状态信息。这里仅用 HTML 描述 Page Object,这种模式还可以用来隐藏 Java swing UI 细节,它可用于所有 UI 框架。

PageObject 六大原则

Selenium 针对 PageObject 的核心思想凝聚出了六大原则,掌握六大原则精髓,才可以进行 PageObject 最佳实践演练:

  • 公共方法代表页面提供的服务
  • 不要暴露页面细节
  • 不要把断言和操作细节混用
  • 方法可以 return 到新打开的页面
  • 不要把整页内容都放到PO 中
  • 相同的行为会产生不同的结果,可以封装不同结果

下面,对上述六大原则进行更详细的实操解释:
原则一:要封装页面中的功能(或者服务),比如点击页面中的元素,可以进入到新的页面,于是,可以为这个服务封装方法“进入新页面”。
原则二:封装细节,对外只提供方法名(或者接口)。
原则三:封装的操作细节中不要使用断言,把断言放到单独的模块中,比如 testcase。
原则四:点击一个按钮会开启新的页面,可以用 return 方法表示跳转,比如return MainPage()表示跳转到新的PO:MainPage。
原则五:只为页面中重要的元素进行 PO 设计,舍弃不重要的内容。
原则六:一个动作可能产生不同结果,比如点击按钮后,可能点击成功,也可能点击失败,为两种结果

基于钉钉打卡的 PO 实战案例


用 Page Object 原则为页面建模,这里涉及三个页面:登录页,首页,工作台页面,考勤打卡页。在代码中创建对应的三个类LoginPage,MainPage,WorkBenchPage,AttendancePage,(本次演示由于涉及功能点较少,所需功能都封装在了MainPage中)

实战代码

目录结构


BasePage 是所有 page object 的父类,它为子类提供公共的方法,比如下面的 BasePage 提供初始化 driver 和退出 driver,代码中在 base_page 模块的 BasePage 类中使用 init 初始方法进行初始化操作,包括 driver 的复用,driver 的赋值,全局等待的设置(隐式等待)等等:

#!/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

由于钉钉是移动端的APP,所以我单独写了个APP类,包含了登录,停止,重启,关闭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是钉钉登录页 page object,它存在两个方法,进入注册 page object 和进入登陆 page object,这里 return 方法返回 page object 实现了页面跳转,比如:goto_register方法return Register,实现从首页跳转到注册页,演示代码中的登录写在了APP类中,这个可以试个人习惯封装,没有固定的模板
MainPage是钉钉登录后的主页,WorkBenchPage是工作台页,AttendancePage是考勤页,本次由于涉及功能较少,统一封装到MainPage类

#!\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')

conftest.py文件是pytest框架特有的,用来做一些用例前置操作

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'))

test_gooffwork和test_gotowork 模块是对上述功能的测试,它独立于 page 类,在 TestIndex 类中只需要调用 page 类提供的方法即可

#!/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('打卡成功')

另外还有一个配置文件夹config用开配置初始化APP和元素的定位
appconfig.yml文件

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

总结:

感谢每一个认真阅读我文章的人!!!

 我个人整理了我这几年软件测试生涯整理的一些技术资料,包含:电子书,简历模块,各种工作模板,面试宝典,自学项目等。欢迎大家评论区留言333免费领取,千万不要错过哦。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/749943.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用selenium模拟登录解决滑块验证问题

目录 1.登录入口 2.点击“账号密码登录” 3.输入账号、密码并点击登录 4.滑块验证过程 5.小结 本次主要是使用selenium模拟登录网页端的TX新闻,本来最开始是模拟请求的,但是某一天突然发现,部分账号需要经过滑块验证才能正常登录&#x…

给大家演示 InsCode Stable Diffusion 美图活动一期

给大家演示 使用 InsCode Stable Diffusion 今天有点无聊,难得领导出差的出差,请假的请假,开会的开会。 心想,此时不摸鱼更待何时。 下面给大家随便验收一番Diffusion ,在这之前也使用了很多,讲真&#xf…

谈谈VPN是什么、类型、使用场景、工作原理

作者:Insist-- 个人主页:insist--个人主页 作者会持续更新网络知识和python基础知识,期待你的关注 前言 本文将讲解VPN是什么、以及它的类型、使用场景、工作原理。 目录 一、VPN是什么? 二、VPN的类型 1、站点对站点VPN 2、…

单片机第一季:零基础6——定时器和计时器

目录 1,单片机定时器原理 2,51单片机定时器/计数器结构 3,定时器配置 4,示例代码-通过定时器控制LED灯间隔1s闪烁 51 单片机有两组定时器/计数器,因为既可以定时,又可以计数,故称之为定时…

【R语言】机器学习-手撕逻辑回归

【R语言】机器学习-手撕逻辑回归 算法原理 逻辑回归是一种常用的分类算法,它在机器学习领域有着广泛的应用。在介绍具体的实现细节之前,我们先来了解一下逻辑回归的算法原理。 sigmoid函数 逻辑回归使用sigmoid函数(也称为逻辑函数&#…

《TCP IP网络编程》第四章

第 4 章 基于 TCP 的服务端/客户端(1) 根据数据传输方式的不同,基于网络协议的套接字一般分为 TCP 套接字和 UDP 套接字。因为 TCP 套接字是面向连接的,因此又被称为基于流(stream)的套接字。 TCP …

烧屏现象对OLED屏幕质量的影响:如何保持画面清晰度?

OLED(Organic Light Emitting Diode)屏幕作为一种高品质、高对比度和鲜艳色彩的显示技术,越来越受到消费者的青睐。然而,一些用户可能会担心OLED屏幕烧屏的问题。本文将探讨OLED屏幕烧屏的原因、如何预防烧屏以及如何进行正确的维…

一个完整的项目是怎么做性能测试?资深8年测试总结...

目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 突然有一天&#…

解决问题:解除HUE下载10万行的限制

解决问题:解除HUE下载10万行的限制 这个目录下的 vim /opt/cloudera/parcels/CDH/lib/hue/apps/beeswax/src/beeswax/conf.py可以调整数量 也可以更改为-1(表示不限制)

有测试媛的IT团队,产品质量更高

我们都知道,在绝大部分技术团队中,往往阳盛阴衰。而测试团队,可能情况要好一些,据部分机构调查来看男女比例在3:2左右。 根据过往经验来看,有测试媛的IT团队,往往软件质量能更好的得到保证(强调…

【单片机】MSP430F5529单片机的Flash读写控制,MSP430 flash 读写

文章目录 内存模型程序 内存模型 https://qq742971636.blog.csdn.net/article/details/108892807 单片机的Flash里面的区域不是全都能写的:https://blog.csdn.net/u014470361/article/details/79297601 找一下手册看看MSP430F5529单片机哪些地址区域能写&#xf…

nginx常用命令以及安装

目录 前言: 安装: 常用命令: 前言: Nginx的设计理念是高性能、稳定性、开放性和易用性。它的并发能力优秀,可以处理数万个并发连接,并且占用较少的资源。此外,Nginx支持热部署,即…

如何将企业联系方式API接口应用于你的移动端APP?

在现代商业世界中,企业需要提供一种快速,高效的方式来让用户获取联系方式。因此,企业联系方式API接口应运而生,它是一种提供了企业联系方式的开放接口,在用户调用时,可以实现即时获取企业的联系方式信息。本…

python-计算两个矩阵的相似度。

余弦相似度 在pytorch中,有一个专门的函数用于计算相似度:torch.cosine_similarity() https://pytorch.org/docs/stable/nn.functional.html#cosine-similarity import torch import torch.nn.functional as F input1 torch.randn(100, 128) input2 t…

【2023,学点儿新Java-32】Java基础小练习:根据圆周率与半径求圆的面积 | 温度转换 | 计算矩形面积 | 判断奇偶数 | 年龄分类

前情提要: 【2023,学点儿新Java-31】测试:整型和浮点型变量的使用 | 附:计算机存储单位(转换关系)| 企业真题:为什么0.10.2不等于0.3【2023,学点儿新Java-30】变量的基本使用&#…

回归预测 | MATLAB实现WOA-CNN-BiGRU鲸鱼算法优化卷积双向门控循环单元多输入单输出回归预测

回归预测 | MATLAB实现WOA-CNN-BiGRU鲸鱼算法优化卷积双向门控循环单元多输入单输出回归预测 目录 回归预测 | MATLAB实现WOA-CNN-BiGRU鲸鱼算法优化卷积双向门控循环单元多输入单输出回归预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 回归预测 | MATLAB实…

安全不“放假”,VR安全教育等你沉浸式体验

暑期开启,孩子们将迎来一段丰富且快乐的时光,暑假虽快乐,但是安全不能“放假”,许多地方开展形式多样的暑假安全宣传活动,想不想沉浸式体验下VR安全教育宣传呢?通过VR演示,身临其境的了解防溺水…

MySQL-概述-数据模型SQL简介

数据库:DataBase(DB),是存储和管理数据的仓库数据库管理系统:DataBase Management System(DBMS),操作和管理数据库的大型软件。SQL:Structured Query Language&#xff0…

「提高你的CSS技能」:15个重要的CSS属性详解

这篇文章介绍了15个重要的CSS属性,旨在提高读者的CSS知识和技能。文章以清晰的方式解释了每个属性的作用和用法,并提供了相应的示例代码。通过这篇文章,读者可以了解到一些有趣且实用的CSS属性。 1:in-range 和:out-of-range 伪类 CSS的:in…

KafKa 3.x(三、Eagle、Kraft、压测调优,源码解析)

6. Kafka-Eagle监控 Kafka-Eagle框架用于监控Kafka集群运行状况。官网https://kafka-eagle.org 6.1 Mysql 前置mysql。 6.2 Kafka环境 关闭集群 kf.sn stop修改vim /bin/kafka-server-start.sh 并同步到其他节点 if [ "x$KAFKA_HEAP_OPTS" "x" ]; t…