基于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设计模式分析
- 用Page Object表示UI
- 减少重复样本代码
- 让变更范围控制在Page Object内
- 本质是面向对象编程
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-8
DEFAULT_SECONDS = 10 class BasePage(object):
"""
第一层:对uiAutomator2进行二次封装,定义一个所有页面都继承的BasePage
封装uiAutomator2基本方法,如:元素定位,元素等待,导航页面等
不需要全部封装,用到多少就封装多少
""" def __init__(self, device):
self.d = device def by_id(self, id_name):
"""通过id定位单个元素"""
try:
self.d.implicitly_wait(DEFAULT_SECONDS)
return self.d(resourceId=id_name)
except Exception as e:
print("页面中没有找到id为%s的元素" % id_name)
raise e def by_id_matches(self, id_name):
"""通过id关键字匹配定位单个元素"""
try:
self.d.implicitly_wait(DEFAULT_SECONDS)
return self.d(resourceIdMatches=id_name)
except Exception as e:
print("页面中没有找到id为%s的元素" % id_name)
raise e def by_class(self, class_name):
"""通过class定位单个元素"""
try:
self.d.implicitly_wait(DEFAULT_SECONDS)
return self.d(className=class_name)
except Exception as e:
print("页面中没有找到class为%s的元素" % class_name)
raise e def by_text(self, text_name):
"""通过text定位单个元素"""
try:
self.d.implicitly_wait(DEFAULT_SECONDS)
return self.d(text=text_name)
except Exception as e:
print("页面中没有找到text为%s的元素" % text_name)
raise e def by_class_text(self, class_name, text_name):
"""通过text和class多重定位某个元素"""
try:
self.d.implicitly_wait(DEFAULT_SECONDS)
return self.d(className=class_name, text=text_name)
except Exception as e:
print("页面中没有找到class为%s、text为%s的元素" % (class_name, text_name))
raise e def by_text_match(self, text_match):
"""通过textMatches关键字匹配定位单个元素"""
try:
self.d.implicitly_wait(DEFAULT_SECONDS)
return self.d(textMatches=text_match)
except Exception as e:
print("页面中没有找到text为%s的元素" % text_match)
raise e def by_desc(self, desc_name):
"""通过description定位单个元素"""
try:
self.d.implicitly_wait(DEFAULT_SECONDS)
return self.d(description=desc_name)
except Exception as e:
print("页面中没有找到desc为%s的元素" % desc_name)
raise e def by_xpath(self, xpath):
"""通过xpath定位单个元素【特别注意:只能用d.xpath,千万不能用d(xpath)】"""
try:
self.d.implicitly_wait(DEFAULT_SECONDS)
return self.d.xpath(xpath)
except Exception as e:
print("页面中没有找到xpath为%s的元素" % xpath)
raise e def by_id_text(self, id_name, text_name):
"""通过id和text多重定位"""
try:
self.d.implicitly_wait(DEFAULT_SECONDS)
return self.d(resourceId=id_name, text=text_name)
except Exception as e:
print("页面中没有找到resourceId、text为%s、%s的元素" % (id_name, text_name))
raise e def find_child_by_id_class(self, id_name, class_name):
"""通过id和class定位一组元素,并查找子元素"""
try:
self.d.implicitly_wait(DEFAULT_SECONDS)
return self.d(resourceId=id_name).child(className=class_name)
except Exception as e:
print("页面中没有找到resourceId为%s、className为%s的元素" % (id_name, class_name))
raise e def is_text_loc(self, text):
"""定位某个文本对象(多用于判断某个文本是否存在)"""
return self.by_text(text_name=text) def is_id_loc(self, id):
"""定位某个id对象(多用于判断某个id是否存在)"""
return self.by_id(id_name=id) def fling_forward(self):
"""当前页面向上滑动"""
return self.d(scrollable=True).fling.vert.forward() def swipe_up(self):
"""当前页面向上滑动,步长为10"""
return self.d(scrollable=True).swipe("up", steps=10) def swipe_down(self):
"""当前页面向下滑动,步长为10"""
return self.d(scrollable=True).swipe("down", steps=10) def swipe_left(self):
"""当前页面向左滑动,步长为10"""
return self.d(scrollable=True).swipe("left", steps=10) def swipe_right(self):
"""当前页面向右滑动,步长为10"""
return self.d(scrollable=True).swipe("right", steps=10)
3.定义各个页面Page
所有页面Page类都继承BasePage。根据PO模式六大原则之一的
- home_page.py
- chat_page.py
- group_page.py
1)home_page.py
# coding:utf-8
from pages.u2_base_page import BasePage class HomePage(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" def msg_icon_obj(self):
"""会话图标"""
return self.by_id(id_name=self.msg_icon) def click_msg_icon(self):
"""点击底部会话图标"""
return self.by_id(id_name=self.msg_icon).click() def click_friend_icon(self):
"""点击底部通讯录图标"""
return self.by_id(id_name=self.friend_icon).click() def click_find_icon(self):
"""点击底部发现图标"""
return self.by_id(id_name=self.find_icon).click() def click_mine_icon(self):
"""点击底部我的图标"""
return self.by_id(id_name=self.mine_icon).click() def click_add_icon(self):
"""点击右上角+号图标"""
return self.by_id(id_name=self.add_icon).click() def click_create_group_btn(self):
"""点击右上角+号图标"""
return self.by_id(id_name=self.create_group_btn).click()
2)chat_page.py
# coding:utf-8
from pages.u2_base_page import BasePage class ChatPage(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" def open_chat_by_name(self, name):
"""根据会话名打开会话"""
return self.by_text(text_name=name).click() def send_text(self, text):
"""发送文本消息"""
return self.by_id(id_name=self.content).send_keys(text) def click_send_button(self):
"""点击发送按钮"""
return self.by_id(id_name=self.send_button).click() def click_bottom_side(self):
"""点击会话界面底部区域、唤起键盘"""
return self.d.click(0.276, 0.973) def click_more_button(self):
"""点击+号按钮"""
return self.by_id(id_name=self.more_button).click() def album_icon_obj(self):
"""相册图标"""
return self.by_id(id_name=self.album_icon) def click_album_icon(self):
"""点击相册图标打开相册"""
return self.by_id(id_name=self.album_icon).click() def select_picture(self, range_int):
"""点击相册中的图片选择图片"""
return self.by_xpath(
'//*[@resource-id="com.zhoulesin.imuikit2:id/recycler"]/android.widget.FrameLayout[%d]' % range_int).click() def click_finish_button(self):
"""点击完成按钮、发送图片"""
return self.by_id(id_name=self.finish_button).click()
3)group_page.py
from pages.u2_base_page import BasePage class GroupPage(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" def select_group_member(self):
"""选择群成员,全部选择"""
friend_list = self.by_id(self.friend_list).child(resourceId=self.friend_list_child)
for i in range(len(friend_list)):
friend_list[i].click() def click_confirm_btn(self):
"""点击确认按钮"""
return self.by_id(id_name=self.confirm_btn).click() def click_more_icon(self):
"""点击群聊设置中右上角的更多图标"""
return self.by_id(id_name=self.more_icon).click() def modify_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() def click_group_icon(self):
"""点击群组图标,进入群组列表"""
return self.by_id(self.group_icon).click()
4.编写测试用例
测试用例实际上是调用各个页面对象组合成的一个业务逻辑集合,中间再加入一些控制结构(选择结构if...else、循环结构for)、断言等,就形成了最终的测试用例。
# coding:utf-8
import random import uiautomator2 as u2
from pages.home_page import HomePage
from pages.chat_page import ChatPage class TestYueYun:
def setup(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) def test_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() # 点击发送按钮
for i in range(1, 10): # 发送10条消息:1-10,范围及发送的内容也可以自定义
self.chat.send_text(i)
self.chat.click_send_button()
self.chat.send_text("测试完成!")
self.chat.click_send_button()
# 返回主页
while not self.home.msg_icon_obj().exists():
self.d.press("back") def test_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() # 点击发送(+)号按钮,弹出相册选项
for i in range(2): # 发送图标的次数
# 判断当相册图标不存在时,点击(+)号从键盘模式切换为选择图片视频等
if not self.chat.album_icon_obj().exists():
self.chat.click_more_button()
self.chat.click_album_icon() # 点击相册图标,进入相册选择图片
for a in range(3): # 一次性选择3张图片
# 从相册child子列表中指定范围内随机选择3张图片
self.chat.select_picture(range_int=random.randint(1, 20))
self.chat.click_finish_button() # 点击发送按钮,发送图片
if not self.chat.album_icon_obj().exists():
self.chat.click_more_button()
self.chat.send_text("测试完成!")
self.chat.click_send_button()
# 返回主页
while not self.home.msg_icon_obj().exists():
self.d.press("back")
5.运行效果
小结
以上就是利用uiautomator2结合PO模式测试移动端APP的一次实践,介绍了:
- PO模式相关概念:六大原则、设计模式、PO封装元素组成、业内常见的分层模型
- GUI自动化测试:为什么要做自动化即自动化的利弊、什么样的项目适合做自动化
- APP自动化测试实践:如何设计项目结构、封装页面基类、定义页面对象、编写测试用例
当然,你还可以借助业内常见的一些PO库,如page_objects,从而更加简便地设计测试框架、组织用例等,但核心思想一直不变,都是为了实现代码逻辑和业务逻辑分离,从而达到灵活复用、以不变应万变的目的。
更多实战干货,欢迎扫码关注!

基于UiAutomator2+PageObject模式开展APP自动化测试实战的更多相关文章
- Android Native App自动化测试实战讲解(上)(基于python)
1.Native App自动化测试及Appuim框架介绍 android平台提供了一个基于java语言的测试框架uiautomator,它一个测试的Java库,包含了创建UI测试的各种API和执行自动 ...
- Android Hybrid App自动化测试实战讲解(基于python)
1.Hybrid App自动化测试概要 什么是Hybrid App? Hybrid App(混合模式移动应用)是指介于web-app.native-app这两者之间的app,兼具“Native App ...
- Android App自动化测试实战(基于Python)(三)
1.Native App自动化测试及Appuim框架介绍 android平台提供了一个基于java语言的测试框架uiautomator,它一个测试的Java库,包含了创建UI测试的各种API和执行自动 ...
- Android Native App自动化测试实战讲解(下)(基于python)
6.Appuim自动化测试框架API讲解与案例实践(三) 如图1,可以在主函数里通过TestSuite来指定执行某一个测试用例: 6.1,scroll():如图2 从图3中可以看到当前页面的所有元素r ...
- UIautomator2框架快速入门App自动化测试
01.APP测试框架比较 常见的APP测试框架 APP测试框架 02.UIAutomator2简介 简介 UIAutomator2是一个可以使用Python对Android设备进行UI自动化的库. ...
- 基于jest和puppeteer的前端自动化测试实战
前端测试现状 经常听到后端同学说“单元测试”,前端写过测试用例的有多少?答案是:并不多,为什么呢?两个主要原因 1.前端属于GUI软件,浏览器众多,兼容问题让人头大,用户量有一定规模的浏览器包括: I ...
- 【Python + ATX基于uiautomator2】之编写unittest自动化测试脚本
不说废话上代码: #!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2018/08/31 09:43 # @Author : zc # @ ...
- 【Python + ATX】之uiautomator2 PageObject模式自动化框架学习
参考文章: 感谢:cynic (linpengcheng) <ATX 基于 ATX-Server 的 UI 自动化测试框架> <ATX-uiautomator2 实现 webview ...
- 【ATX学习大纲】【ATX基于uiautomator2+Python学习】之Android自动化
github学习地址:https://github.com/openatx/uiautomator2 <_io.TextIOWrapper name='<stderr>' mode= ...
随机推荐
- 【ACM程序设计】动态规划 第二篇 LCS&LIS问题
动态规划 P1439 [模板]最长公共子序列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 题目描述 给出 1,2,-,n 的两个排列 P1 和 P2 ,求它们的最长公共子序列. ...
- 第06组 Beta冲刺 (3/5)
目录 1.1 基本情况 1.2 冲刺概况汇报 1.郝雷明 2. 方梓涵 3.曾丽莉 4.杜筱 5. 董翔云 6.黄少丹 7.鲍凌函 8.詹鑫冰 9.曹兰英 10.吴沅静 1.3 冲刺成果展示 1.1 ...
- vue组件传参的方法--bus事件总线
定义:事件总线是实现vue任意组件之前传递参数的一种编程技巧,本质上就是组件的自定义事件.事件总线有很多种写法,具体的思路就是创造一个大家都可以访问到的公共的属性,在这个公共的属性上面可以调用$on, ...
- element-ui table组件使用v-if时的问题
element-ui项目中经常遇到需要使用v-if指令来根据情况动态显示隐藏某些列情况,这时就会出现滚动条样式异常.列错乱.列宽错乱等问题 解决办法:在el-table上添加:key="Ma ...
- Docker容器(centos)安装zabbix
zabbix是一个基于WEB界面提供分布式系统监视以及网络监视功能的企业级的开源解决方案.--百度百科 zabbix介绍 zabbix主要有zabbix-server及zabbix-agent组成,z ...
- Caller 服务调用 - Dapr
前言 上一篇我们讲了使用HttpClient的方式调用,那么如果我们现在需要更换为通过dapr实现服务调用,我们需要做哪些事情呢? Caller.Dapr 入门 如果我们的项目原本使用的是Caller ...
- TypeScript(3)基础类型
基础类型 TypeScript 支持与 JavaScript 几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用. 布尔值 最基本的数据类型就是简单的true/false值,在JavaScri ...
- 掘地三尺搞定 Redis 与 MySQL 数据一致性问题
Redis 拥有高性能的数据读写功能,被我们广泛用在缓存场景,一是能提高业务系统的性能,二是为数据库抵挡了高并发的流量请求,点我 -> 解密 Redis 为什么这么快的秘密. 把 Redis 作 ...
- java反射之-Javabean与Map的互转
1.BeanUntils工具类的准备 /** * @ClassName: BeanUtils * @Description: * @Author: songwp * @Date: 9:02 2022/ ...
- Python音频处理基础知识,这不是轻轻松松~~~
大家好鸭,我是小熊猫 咱今天来讲一讲音频处理的基础知识上才艺~~~ 1.声音的基础 2.python读取.wav音频 欢迎加入白嫖Q群:660193417### import wave import ...