基于UiAutomator2+PageObject模式开展APP自动化测试实战

简介: 基于uiautomator2自动化测试框架+PageObject面向页面对象模式开展移动APP的自动化测试实践。

前言

在上一篇《APP自动化测试框架-UiAutomator2基础》中,重点介绍了uiautomator2的项目组成、运行原理、环境搭建及元素定位等基础入门知识,本篇将介绍如何基于uiautomator2设计PageObject模式(以下简称PO模式)、开展移动APP的自动化测试实践。

一、PO模式简介

1.起源

PO模式是国外大神Martin Fowler于2013年提出来的一种设计模式,其基本思想是强调代码逻辑和业务逻辑相分离。https://martinfowler.com/bliki/PageObject.html

2.PO六大原则

翻译成中文就是:

  • 公共方法表示页面提供的服务
  • 尽量不要暴露页面的内部实现
  • 页面中不要加断言,断言加载
  • 方法返回另外的页面对象
  • 不需要封装全部的页面元素
  • 相同的行为、不同的结果,需要封装成不同的方法

3.PO设计模式分析

  1. 用Page Object表示UI
  2. 减少重复样本代码
  3. 让变更范围控制在Page Object内
  4. 本质是面向对象编程

4.PO封装的主要组成元素

  • Driver对象:完成对WEB、Android、iOS、接口的驱动
  • Page对象:完成对页面的封装
  • 测试用例:调用Page对象实现业务并断言
  • 数据封装:配置文件和数据驱动
  • Utils:其他功能/工具封装,改善原生框架不足

5.业内常见的分层模型

1)四层模型

  • Driver层完成对webdriver常用方法的二次封装,如:定位元素方法;
  • Elements层:存放元素属性值,如图标、按钮的resourceId、className等;
  • Page层:存放页面对象,通常一个UI界面封装一个对象类;
  • Case层:调用各个页面对象类,组合业务逻辑、形成测试用例;

2)三层模型(推荐)

四层模型与三层模型唯一的区别就是将Page层与Elements层存放在一起,各个页面对象文件同时包含当前页面中各个图标、按钮的resourceId、className等属性值,以便随时调用;

二、GUI自动化测试二三事

1.什么是自动化

自动化顾名思义就是把人对软件的操作行为通过代码或工具转换为机器执行测试的过程或实践。

2.为什么要做自动化

这个可说的内容就太多了,不做过多赘述,详情可参照我整理的《软件测试52讲》课堂笔记中的内容:

3.什么样的项目适合做自动化

  • 需求稳定,不会频繁变更(尤其是GUI测试,页面布局及元素不能频繁变化)
  • 研发和维护周期长,需要频繁执行回归测试
  • 手工测试无法实现或成本高,需要用自动化代替实现
  • 需要重复运行的测试场景
  • ......

三、APP自动化测试实战

1.设计项目结构

2.封装BasePage

即Driver层,对uiautomator2进行二次封装,所有Page类都会直接或间接继承BasePage# coding:utf-8DEFAULT_SECONDS=10classBasePage(object):
"""    第一层:对uiAutomator2进行二次封装,定义一个所有页面都继承的BasePage    封装uiAutomator2基本方法,如:元素定位,元素等待,导航页面等    不需要全部封装,用到多少就封装多少    """def__init__(self, device):
self.d=devicedefby_id(self, id_name):
"""通过id定位单个元素"""try:
self.d.implicitly_wait(DEFAULT_SECONDS)
returnself.d(resourceId=id_name)
exceptExceptionase:
print("页面中没有找到id为%s的元素"%id_name)
raiseedefby_id_matches(self, id_name):
"""通过id关键字匹配定位单个元素"""try:
self.d.implicitly_wait(DEFAULT_SECONDS)
returnself.d(resourceIdMatches=id_name)
exceptExceptionase:
print("页面中没有找到id为%s的元素"%id_name)
raiseedefby_class(self, class_name):
"""通过class定位单个元素"""try:
self.d.implicitly_wait(DEFAULT_SECONDS)
returnself.d(className=class_name)
exceptExceptionase:
print("页面中没有找到class为%s的元素"%class_name)
raiseedefby_text(self, text_name):
"""通过text定位单个元素"""try:
self.d.implicitly_wait(DEFAULT_SECONDS)
returnself.d(text=text_name)
exceptExceptionase:
print("页面中没有找到text为%s的元素"%text_name)
raiseedefby_class_text(self, class_name, text_name):
"""通过text和class多重定位某个元素"""try:
self.d.implicitly_wait(DEFAULT_SECONDS)
returnself.d(className=class_name, text=text_name)
exceptExceptionase:
print("页面中没有找到class为%s、text为%s的元素"% (class_name, text_name))
raiseedefby_text_match(self, text_match):
"""通过textMatches关键字匹配定位单个元素"""try:
self.d.implicitly_wait(DEFAULT_SECONDS)
returnself.d(textMatches=text_match)
exceptExceptionase:
print("页面中没有找到text为%s的元素"%text_match)
raiseedefby_desc(self, desc_name):
"""通过description定位单个元素"""try:
self.d.implicitly_wait(DEFAULT_SECONDS)
returnself.d(description=desc_name)
exceptExceptionase:
print("页面中没有找到desc为%s的元素"%desc_name)
raiseedefby_xpath(self, xpath):
"""通过xpath定位单个元素【特别注意:只能用d.xpath,千万不能用d(xpath)】"""try:
self.d.implicitly_wait(DEFAULT_SECONDS)
returnself.d.xpath(xpath)
exceptExceptionase:
print("页面中没有找到xpath为%s的元素"%xpath)
raiseedefby_id_text(self, id_name, text_name):
"""通过id和text多重定位"""try:
self.d.implicitly_wait(DEFAULT_SECONDS)
returnself.d(resourceId=id_name, text=text_name)
exceptExceptionase:
print("页面中没有找到resourceId、text为%s、%s的元素"% (id_name, text_name))
raiseedeffind_child_by_id_class(self, id_name, class_name):
"""通过id和class定位一组元素,并查找子元素"""try:
self.d.implicitly_wait(DEFAULT_SECONDS)
returnself.d(resourceId=id_name).child(className=class_name)
exceptExceptionase:
print("页面中没有找到resourceId为%s、className为%s的元素"% (id_name, class_name))
raiseedefis_text_loc(self, text):
"""定位某个文本对象(多用于判断某个文本是否存在)"""returnself.by_text(text_name=text)
defis_id_loc(self, id):
"""定位某个id对象(多用于判断某个id是否存在)"""returnself.by_id(id_name=id)
deffling_forward(self):
"""当前页面向上滑动"""returnself.d(scrollable=True).fling.vert.forward()
defswipe_up(self):
"""当前页面向上滑动,步长为10"""returnself.d(scrollable=True).swipe("up", steps=10)
defswipe_down(self):
"""当前页面向下滑动,步长为10"""returnself.d(scrollable=True).swipe("down", steps=10)
defswipe_left(self):
"""当前页面向左滑动,步长为10"""returnself.d(scrollable=True).swipe("left", steps=10)
defswipe_right(self):
"""当前页面向右滑动,步长为10"""returnself.d(scrollable=True).swipe("right", steps=10)

3.定义各个页面Page

所有页面Page类都继承BasePage。根据PO模式六大原则之一的“不需要封装全部的页面元素”,用到多少页面元素就封装多少。例如:当前待测APP有3个界面,则定义3个页面Page:

  • home_page.py
  • chat_page.py
  • group_page.py

1)home_page.py

# coding:utf-8frompages.u2_base_pageimportBasePageclassHomePage(BasePage):
def__init__(self, device):
super(YueYunHome, self).__init__(device)
self.msg_icon="com.zhoulesin.imuikit2:id/icon_msg"self.friend_icon="com.zhoulesin.imuikit2:id/icon_friend"self.find_icon="com.zhoulesin.imuikit2:id/icon_find"self.mine_icon="com.zhoulesin.imuikit2:id/icon_mine"self.add_icon="com.zhoulesin.imuikit2:id/iv_chat_add"self.create_group_btn="com.zhoulesin.imuikit2:id/ll_create_group"self.chat_list="com.zhoulesin.imuikit2:id/rv_message_list"self.chat_list_child="com.zhoulesin.imuikit2:id/ll_content"defmsg_icon_obj(self):
"""会话图标"""returnself.by_id(id_name=self.msg_icon)
defclick_msg_icon(self):
"""点击底部会话图标"""returnself.by_id(id_name=self.msg_icon).click()
defclick_friend_icon(self):
"""点击底部通讯录图标"""returnself.by_id(id_name=self.friend_icon).click()
defclick_find_icon(self):
"""点击底部发现图标"""returnself.by_id(id_name=self.find_icon).click()
defclick_mine_icon(self):
"""点击底部我的图标"""returnself.by_id(id_name=self.mine_icon).click()
defclick_add_icon(self):
"""点击右上角+号图标"""returnself.by_id(id_name=self.add_icon).click()
defclick_create_group_btn(self):
"""点击右上角+号图标"""returnself.by_id(id_name=self.create_group_btn).click()

2)chat_page.py

# coding:utf-8frompages.u2_base_pageimportBasePageclassChatPage(BasePage):
def__init__(self, device):
super(SingleChat, self).__init__(device)
self.msg_icon="com.zhoulesin.imuikit2:id/icon_msg"self.friend_icon="com.zhoulesin.imuikit2:id/icon_friend"self.find_icon="com.zhoulesin.imuikit2:id/icon_find"self.mine_icon="com.zhoulesin.imuikit2:id/icon_mine"self.content="com.zhoulesin.imuikit2:id/et_content"self.send_button="com.zhoulesin.imuikit2:id/btn_send"self.more_button="com.zhoulesin.imuikit2:id/btn_more"self.album_icon="com.zhoulesin.imuikit2:id/photo_layout"self.finish_button="com.zhoulesin.imuikit2:id/btn_ok"defopen_chat_by_name(self, name):
"""根据会话名打开会话"""returnself.by_text(text_name=name).click()
defsend_text(self, text):
"""发送文本消息"""returnself.by_id(id_name=self.content).send_keys(text)
defclick_send_button(self):
"""点击发送按钮"""returnself.by_id(id_name=self.send_button).click()
defclick_bottom_side(self):
"""点击会话界面底部区域、唤起键盘"""returnself.d.click(0.276, 0.973)
defclick_more_button(self):
"""点击+号按钮"""returnself.by_id(id_name=self.more_button).click()
defalbum_icon_obj(self):
"""相册图标"""returnself.by_id(id_name=self.album_icon)
defclick_album_icon(self):
"""点击相册图标打开相册"""returnself.by_id(id_name=self.album_icon).click()
defselect_picture(self, range_int):
"""点击相册中的图片选择图片"""returnself.by_xpath(
'//*[@resource-id="com.zhoulesin.imuikit2:id/recycler"]/android.widget.FrameLayout[%d]'%range_int).click()
defclick_finish_button(self):
"""点击完成按钮、发送图片"""returnself.by_id(id_name=self.finish_button).click()

3)group_page.py

frompages.u2_base_pageimportBasePageclassGroupPage(BasePage):
def__init__(self, device):
super().__init__(device)
self.friend_list="com.zhoulesin.imuikit2:id/rv_friend_list"self.friend_list_child="com.zhoulesin.imuikit2:id/iv_select"self.confirm_btn="com.zhoulesin.imuikit2:id/tv_confirm"self.more_icon="com.zhoulesin.imuikit2:id/img_right"self.group_name="群聊名称"self.group_name_edit_context="com.zhoulesin.imuikit2:id/et_group_name"self.finish_btn="com.zhoulesin.imuikit2:id/tv_btn"self.group_icon="com.zhoulesin.imuikit2:id/ll_my_group"self.group_list="com.zhoulesin.imuikit2:id/rv_group_list"self.group_list_child="com.zhoulesin.imuikit2:id/name"defselect_group_member(self):
"""选择群成员,全部选择"""friend_list=self.by_id(self.friend_list).child(resourceId=self.friend_list_child)
foriinrange(len(friend_list)):
friend_list[i].click()
defclick_confirm_btn(self):
"""点击确认按钮"""returnself.by_id(id_name=self.confirm_btn).click()
defclick_more_icon(self):
"""点击群聊设置中右上角的更多图标"""returnself.by_id(id_name=self.more_icon).click()
defmodify_group_name(self, group_name):
"""点击群聊设置中右上角的更多图标"""self.by_text(self.group_name).click()
self.by_id(self.group_name_edit_context).send_keys(group_name)
self.by_id(self.finish_btn).click()
defclick_group_icon(self):
"""点击群组图标,进入群组列表"""returnself.by_id(self.group_icon).click()

4.编写测试用例

测试用例实际上是调用各个页面对象组合成的一个业务逻辑集合,中间再加入一些控制结构(选择结构if...else、循环结构for)、断言等,就形成了最终的测试用例。

# coding:utf-8importrandomimportuiautomator2asu2frompages.home_pageimportHomePagefrompages.chat_pageimportChatPageclassTestYueYun:
defsetup(self):
device='tkqkssgirgaipblj'# 设备序列号apk='com.zhoulesin.imuikit2'# 包名self.d=u2.connect(device)
self.d.app_start(apk)
self.home=HomePage(self.d)
self.chat=ChatPage(self.d)
deftest_send_msg(self):
"""测试发送文本消息"""self.home.click_msg_icon()  # 点击底部消息图标,进入主页self.chat.open_chat_by_name("张三")  # 点开名为“张三”的联系人会话self.chat.click_bottom_side()  # 点击底部区域,唤起键盘self.chat.send_text("开始发送消息...")  # 输入框输入文字self.chat.click_send_button()  # 点击发送按钮foriinrange(1, 10):  # 发送10条消息:1-10,范围及发送的内容也可以自定义self.chat.send_text(i)
self.chat.click_send_button()
self.chat.send_text("测试完成!")
self.chat.click_send_button()
# 返回主页whilenotself.home.msg_icon_obj().exists():
self.d.press("back")
deftest_send_picture(self):
"""测试发送图片"""self.home.click_msg_icon()  # 点击底部消息图标,进入主页self.chat.open_chat_by_name("群聊一")  # 点开名为“群聊一”的会话self.chat.click_bottom_side()  # 点击底部区域,唤起键盘self.chat.send_text("测试发送图片...")  # 输入框输入文字self.chat.click_send_button()  # 点击发送(+)号按钮,弹出相册选项foriinrange(2):  # 发送图标的次数# 判断当相册图标不存在时,点击(+)号从键盘模式切换为选择图片视频等ifnotself.chat.album_icon_obj().exists():
self.chat.click_more_button()
self.chat.click_album_icon()  # 点击相册图标,进入相册选择图片forainrange(3):  # 一次性选择3张图片# 从相册child子列表中指定范围内随机选择3张图片self.chat.select_picture(range_int=random.randint(1, 20))
self.chat.click_finish_button()  # 点击发送按钮,发送图片ifnotself.chat.album_icon_obj().exists():
self.chat.click_more_button()
self.chat.send_text("测试完成!")
self.chat.click_send_button()
# 返回主页whilenotself.home.msg_icon_obj().exists():
self.d.press("back")

5.运行效果

此处为语雀视频卡片,点击链接查看:2c94cc3fb316fcb93f1b910d4056f5d2.mp4

小结

以上就是利用uiautomator2结合PO模式测试移动端APP的一次实践,介绍了:

  • PO模式相关概念:六大原则、设计模式、PO封装元素组成、业内常见的分层模型
  • GUI自动化测试:为什么要做自动化即自动化的利弊、什么样的项目适合做自动化
  • APP自动化测试实践:如何设计项目结构、封装页面基类、定义页面对象、编写测试用例

当然,你还可以借助业内常见的一些PO库,如page_objects,从而更加简便地设计测试框架、组织用例等,但核心思想一直不变,都是为了实现代码逻辑和业务逻辑分离,从而达到灵活复用、以不变应万变的目的。

相关文章
|
3月前
|
机器学习/深度学习 PyTorch 算法框架/工具
目标检测实战(一):CIFAR10结合神经网络加载、训练、测试完整步骤
这篇文章介绍了如何使用PyTorch框架,结合CIFAR-10数据集,通过定义神经网络、损失函数和优化器,进行模型的训练和测试。
214 2
目标检测实战(一):CIFAR10结合神经网络加载、训练、测试完整步骤
|
2月前
|
测试技术 持续交付 UED
软件测试的艺术:确保质量的实战策略
在软件开发的舞台上,测试是那把确保每个功能如交响乐般和谐奏响的指挥棒。本文将深入探讨软件测试的重要性、基本类型以及如何设计高效的测试策略。我们将通过一个实际的代码示例,展示如何运用这些策略来提升软件质量和用户体验。
|
2月前
|
Java 测试技术 持续交付
【入门思路】基于Python+Unittest+Appium+Excel+BeautifulReport的App/移动端UI自动化测试框架搭建思路
本文重点讲解如何搭建App自动化测试框架的思路,而非完整源码。主要内容包括实现目的、框架设计、环境依赖和框架的主要组成部分。适用于初学者,旨在帮助其快速掌握App自动化测试的基本技能。文中详细介绍了从需求分析到技术栈选择,再到具体模块的封装与实现,包括登录、截图、日志、测试报告和邮件服务等。同时提供了运行效果的展示,便于理解和实践。
151 4
【入门思路】基于Python+Unittest+Appium+Excel+BeautifulReport的App/移动端UI自动化测试框架搭建思路
|
2月前
|
JSON JavaScript 前端开发
harmony-chatroom 自研纯血鸿蒙OS Next 5.0聊天APP实战案例
HarmonyOS-Chat是一个基于纯血鸿蒙OS Next5.0 API12实战开发的聊天应用程序。这个项目使用了ArkUI和ArkTS技术栈,实现了类似微信的消息UI布局、输入框光标处插入文字、emoji表情图片/GIF动图、图片预览、红包、语音/位置UI、长按语音面板等功能。
220 2
|
2月前
|
JSON Java 测试技术
SpringCloud2023实战之接口服务测试工具SpringBootTest
SpringBootTest同时集成了JUnit Jupiter、AssertJ、Hamcrest测试辅助库,使得更容易编写但愿测试代码。
86 3
|
3月前
|
机器学习/深度学习 编解码 监控
目标检测实战(六): 使用YOLOv8完成对图像的目标检测任务(从数据准备到训练测试部署的完整流程)
这篇文章详细介绍了如何使用YOLOv8进行目标检测任务,包括环境搭建、数据准备、模型训练、验证测试以及模型转换等完整流程。
4831 1
目标检测实战(六): 使用YOLOv8完成对图像的目标检测任务(从数据准备到训练测试部署的完整流程)
|
3月前
|
PyTorch 算法框架/工具 计算机视觉
目标检测实战(二):YoloV4-Tiny训练、测试、评估完整步骤
本文介绍了使用YOLOv4-Tiny进行目标检测的完整流程,包括模型介绍、代码下载、数据集处理、网络训练、预测和评估。
265 2
目标检测实战(二):YoloV4-Tiny训练、测试、评估完整步骤
|
2月前
|
缓存 测试技术 Apache
告别卡顿!Python性能测试实战教程,JMeter&Locust带你秒懂性能优化💡
告别卡顿!Python性能测试实战教程,JMeter&Locust带你秒懂性能优化💡
81 1
|
3月前
|
Java 程序员 应用服务中间件
「测试线排查的一些经验-中篇」&& 调试日志实战
「测试线排查的一些经验-中篇」&& 调试日志实战
41 1
「测试线排查的一些经验-中篇」&& 调试日志实战
|
2月前
|
前端开发 数据管理 测试技术
前端自动化测试:Jest与Cypress的实战应用与最佳实践
【10月更文挑战第27天】本文介绍了前端自动化测试中Jest和Cypress的实战应用与最佳实践。Jest适合React应用的单元测试和快照测试,Cypress则擅长端到端测试,模拟用户交互。通过结合使用这两种工具,可以有效提升代码质量和开发效率。最佳实践包括单元测试与集成测试结合、快照测试、并行执行、代码覆盖率分析、测试环境管理和测试数据管理。
108 2