写点什么

PO 设计模式全攻略,在 UI 自动化中的实践总结(以企业微信为例)

作者:测试人
  • 2025-07-04
    北京
  • 本文字数:3309 字

    阅读完需:约 11 分钟

一、什么是 PO 设计模式?

PO(PageObject)设计模式将某个页面的所有元素对象定位和对元素对象的操作封装成一个 Page 类,即一个 py 文件,并以页面为单位来写测试用例,实现页面对象和测试用例的分离,若元素发生变化,只需要进入对应的 Page 类,更新元素定位即可,不用修改用例。


二、在什么样的场景下使用 PO 设计模式?

随着时间的推移,需要维护的页面越来越多时,如果使用传统的设计模式把测不同页面上的所有步骤写在同一个模块里面,会显得笨重。如果页面发生了改变,对应的脚本也要发生改变,难以维护就可以体现出来了,所以使用的 PO 设计模式会方便很多。

三、PO 模式的六大原则

(1)一个 public 方法代表一个公共的服务。就是说一个方法代替页面上的某个操作(公共方法表示页面提供的服务)

(2)PageObject 中的方法细节不可暴露在外,通过提供公共服务接口的形式提供给外部(不要暴露页面的细节)

(3)一般不需要在 PageObject 中断言(Page 设计中不要出现断言,应该写在测试用例类中)

(4)当有页面跳转的操作时候,执行这个方法时应该在方法结束返回时能够跳转到另一个页面中(方法应该返回其他的 Page 对象)

(5)我们只需要对页面中我们需要的重要的内容进行封装(不要去代表整个 page,如果一个页面中有很多功能,只需要对重点功能封装方法即可)

(6)页面中相同的组件,但是不同的操作应该要被拆成不同的方法进行封装(不同的结果返回不同的方法,不同的模式)

四、PO 模式页面层级

PO 模式可以把一个页面分为二个层级:对象操作层、业务层。

(1)对象操作层:封装定位元素,封装对元素的操作。

(2)业务层:将一个或多个操作组合起来完成一个业务功能。

五、PO 模式实战

以下是以企业微信-添加成员为例-最终打印成员列表

传统的 web 自动化脚本

(1)传统的 web 脚本,把 driver 实例化、对象操作以及业务逻辑全部写进一个模块里面;这次举例的测试用例只是涉及到两个页面,如果遇到到更加复杂的测试用例时,把全部的代码写进同一个模块里,这样就显得很臃肿;当有上百个用例,几十个页面的时候,我们会在测试用例中重复的使用到页面当中的元素和操作。当其中的页面发生变化时,我们需要在多个用例中去修改。这种情况下,代码多且乱,维护成本也不低。

(2)使用 PO 设计模式编写测试用例时,通过分层将测试代码和页面元素分开,有效的命名模块名称可以更加清晰知道所操作功能模块以及操作的 UI 元素,哪个页面的数据发生改变时,就在对应的页面进行修改即可;

'''测试用例'''
class TestDemo:
def setup(self):
self.driver = webdriver.Chrome()
self.driver.implicitly_wait(5)
self.driver.maximize_window()
def teardown(self):
self.driver.quit()
def test_add_mumber(self):
# 扫码登录界面
self.driver.get("https://work.weixin.qq.com/wework_admin/loginpage_wx")
# 创建data.yaml文件,设置编码
path = os.path.dirname(os.path.abspath(__file__))
data_file = os.sep.join([path, "../data/data.yaml"])
with open(data_file, encoding="UTF-8") as f:
# 获取数据
yaml_data = yaml.safe_load(f)
# 遍历
for cookie in yaml_data:
# 设置cookie
self.driver.add_cookie(cookie)
# 访问首页
self.driver.get("https://work.weixin.qq.com/wework_admin/frame")
#点击添加成员
self.driver.find_element_by_xpath("//*[@id='_hmt_click']/div[1]/div[4]/div[2]/a[1]/div/span[2]").click()
time.sleep(3)
#输入用户名
self.driver.find_element_by_id("username").send_keys("haha")
#输入别名
self.driver.find_element_by_id("memberAdd_english_name").send_keys("哈哈哈")
#输入账号
self.driver.find_element_by_id("memberAdd_acctid").send_keys("haha001")
# self.driver.find_element_by_xpath("//*[@id='js_contacts72']/div/div[2]/div/div[4]/div/form/div[2]/div[1]/div[3]/div[2]/label[2]/input").click()
#选择性别为“女”
self.driver.find_element_by_css_selector("#js_contacts72 > div > div.member_colRight > div > div:nth-child(4) > div > form > div.member_edit_formWrap > div:nth-child(1) > div.member_edit_item.member_edit_item_Radios > div.member_edit_item_right > label:nth-child(2) > input").click()
#输入手机号码
self.driver.find_element_by_id("memberAdd_phone").send_keys("15676006789")
#输入座机
self.driver.find_element_by_id("memberAdd_telephone").send_keys("0770851")
#输入邮箱
self.driver.find_element_by_id("memberAdd_mail").send_keys("1234@qq.com")
#输入地址
self.driver.find_element_by_id("memberEdit_address").send_keys("xxx")
#点击保存
self.driver.find_element_by_css_selector("#js_contacts72 > div > div.member_colRight > div > div:nth-child(4) > div > form > div:nth-child(3) > a.qui_btn.ww_btn.js_btn_save").click()
time.sleep(2)
复制代码

PO 项目目录结构:


clipboard.png

(1)项目开始前,拿到一份需求时,需要做需求分析,划分模块,编写测试用例。此次测试用例的流程,进入主页(MainPage),点击添加成员,进入添加成员页面,点击添加,点击保存,显示所有的成员列表,最后做断言,验证测试用例执行是否成功。

(2)编写测试用例开始之前,建立时序图,便于思考,时序图如下图所示:


clipboard.png

(3)测试用例的要素:

  • 前置条件

  • 执行步骤

  • 数据检查及断言

'''
添加成员测试用例
'''
class TestAddMumber:
def setup(self):
# 实例化
self.main_page = MainPage()
# 实现测试数据和页面对象分离
@pytest.mark.parametrize("username,accid,phone", [("hxc2", "862", "15676000002")])
def test_add_mumber(self, username, accid, phone):
# 1.跳转到添加成员页面 2.添加成员 3.获取成员列表
name_list = self.main_page.goto_add_number().add_member(username, accid, phone).get_contact_list()
assert username in name_list
复制代码

(2)main_page.py 该模块就是相当于该测试的首页,该成员页面有添加成员、导入通讯录、打卡等功能,以下以点击“添加成员”为例,点击“添加成员”之后进入到通讯录页面,添加成员所在的页面和通讯录不在同一个页面上,当有页面跳转的操作时候,在函数后面需要返回要跳转页面的实例化对象,如下图代码中的 AddMumberPage。

class MainPage(BasePage):
'''使用公共方法代表UI所提供的功能'''
def goto_add_number(self):
#点击添加成员
self.driver.find_element(By.CSS_SELECTOR,".ww_indexImg_AddMember").click()
'''
跳转到添加成员页面
:return:
'''
#返回要跳转页面的实例化对象
return AddMumberPage(self.driver)
复制代码

(3)添加成员,在该模块中定义 add_mumber 接口,把定位元素以及对应的操作封装在 add_member 里,使用时调用 add_mumber 接口即可。PO 中的方法细节不可暴露在外,通过提供公共服务接口的形式提供给外部

#调用时要注意格式(元组),self.driver.find_element需要*号将元组解包。
class AddMumberPage(BasePage):
# 设定为元祖
#__是私有,页面元素不需要让业务了解,所以要加私有
#元素定位
__ele_username = (By.ID, "username")
ele_accid = (By.ID, "memberAdd_acctid")
ele_phone = (By.ID, "memberAdd_phone")
webdriver.Firefox()
#元素对象
def add_member(self, username, accid, phone):
'''
*是解元祖,self.driver.find_element(*self.ele_username)等同于self.driver.find_element(By.ID, "username")
'''
# 填写用户名
self.find(*self.__ele_username).send_keys(username)
# 填写账号
self.driver.find_element(*self.ele_accid).send_keys(accid)
# 填写手机号
self.driver.find_element(*self.ele_phone).send_keys(phone)
# 点击保存
self.driver.find_element(By.CSS_SELECTOR, ".js_btn_save").click()
'''
页面的return分成两个部分
1.其他页面的实例
2.用例所需要的断言
注意:不要写成ContactPage,这个是类
:return:
'''
# 返回通讯实例对象
return ContactPage(self.driver)
复制代码


用户头像

测试人

关注

专注于软件测试开发 2022-08-29 加入

霍格沃兹测试开发学社,测试人社区:https://ceshiren.com/t/topic/22284

评论

发布
暂无评论
PO设计模式全攻略,在 UI 自动化中的实践总结(以企业微信为例)_软件测试_测试人_InfoQ写作社区