基于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天前
|
Kubernetes 监控 安全
Kubernetes实战:集群管理与自动化技术详解
【6月更文挑战第27天】Kubernetes实战聚焦集群自动化管理,涵盖核心概念如主从架构、API Server及工作节点,强调自动扩缩容、RBAC安全控制与日志监控。通过IaC工具如Helm实现配置自动化,结合Prometheus等进行持续监控,强调安全策略与资源优化,展现K8s在现代应用管理中的威力。
|
8天前
|
SQL 搜索推荐 Android开发
AB测试实战(一)
AB测试是一种数据驱动的产品优化方法,用于比较不同版本的网页、应用界面或营销策略的效果。
|
1月前
|
运维 监控 安全
构建高效自动化运维体系:Ansible与Docker的协同实战
【5月更文挑战第25天】 在当今快速迭代的软件发布环境中,自动化运维成为确保部署效率和可靠性的关键。本文通过深入分析Ansible和Docker技术,探索它们如何协同工作以构建一个高效的自动化运维体系。文章不仅介绍了Ansible的配置管理功能和Docker容器化的优势,还详细阐述了将两者结合的实践策略,旨在帮助读者理解并实现更智能、更灵活的基础设施管理。
|
4天前
|
数据采集 Web App开发 JavaScript
Puppeteer实战指南:自动化抓取网页中的图片资源
Puppeteer实战指南:自动化抓取网页中的图片资源
|
8天前
|
测试技术 Python
AB测试实战(二)
AB测试是一种数据驱动的产品优化方法,用于比较不同版本的网页、应用界面或营销策略的效果。
|
10天前
|
开发框架 移动开发 JavaScript
SpringCloud微服务实战——搭建企业级开发框架(四十七):【移动开发】整合uni-app搭建移动端快速开发框架-添加Axios并实现登录功能
在uni-app中,使用axios实现网络请求和登录功能涉及以下几个关键步骤: 1. **安装axios和axios-auth-refresh**: 在项目的`package.json`中添加axios和axios-auth-refresh依赖,可以通过HBuilderX的终端窗口运行`yarn add axios axios-auth-refresh`命令来安装。 2. **配置自定义常量**: 创建`project.config.js`文件,配置全局常量,如API基础URL、TenantId、APP_CLIENT_ID和APP_CLIENT_SECRET等。
|
18天前
|
设计模式 Java 测试技术
Java8实战-重构、测试和调试(二)
Java8实战-重构、测试和调试(二)
24 2
|
21天前
|
运维 DataWorks Oracle
DataWorks产品使用合集之在标准模式下,当同步Oracle的表或视图时,是否需要在源端的测试和生产环境中都存在要同步的表或视图
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
16 3
|
2天前
|
消息中间件 缓存 中间件
【赠书活动 - 第1期】- 测试工程师Python开发实战(异步图书出品)| 文末送书
【赠书活动 - 第1期】- 测试工程师Python开发实战(异步图书出品)| 文末送书
|
24天前
|
存储 SQL 运维
使用PowerShell进行自动化脚本编写:入门与实战
【6月更文挑战第6天】本文介绍了PowerShell作为Windows系统管理的自动化工具,用于提升效率和减少错误。内容涵盖PowerShell基础,如变量、命令执行、管道、条件和循环语句、函数。实战案例展示了如何用PowerShell脚本进行文件备份。此外,还提及PowerShell的进阶功能,如模块、远程管理和与其他工具集成。学习和应用PowerShell能有效提升IT运维自动化水平。