前言

在上一篇《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-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自动化测试实战的更多相关文章

  1. Android Native App自动化测试实战讲解(上)(基于python)

    1.Native App自动化测试及Appuim框架介绍 android平台提供了一个基于java语言的测试框架uiautomator,它一个测试的Java库,包含了创建UI测试的各种API和执行自动 ...

  2. Android Hybrid App自动化测试实战讲解(基于python)

    1.Hybrid App自动化测试概要 什么是Hybrid App? Hybrid App(混合模式移动应用)是指介于web-app.native-app这两者之间的app,兼具“Native App ...

  3. Android App自动化测试实战(基于Python)(三)

    1.Native App自动化测试及Appuim框架介绍 android平台提供了一个基于java语言的测试框架uiautomator,它一个测试的Java库,包含了创建UI测试的各种API和执行自动 ...

  4. Android Native App自动化测试实战讲解(下)(基于python)

    6.Appuim自动化测试框架API讲解与案例实践(三) 如图1,可以在主函数里通过TestSuite来指定执行某一个测试用例: 6.1,scroll():如图2 从图3中可以看到当前页面的所有元素r ...

  5. UIautomator2框架快速入门App自动化测试

    01.APP测试框架比较 常见的APP测试框架   APP测试框架 02.UIAutomator2简介 简介 UIAutomator2是一个可以使用Python对Android设备进行UI自动化的库. ...

  6. 基于jest和puppeteer的前端自动化测试实战

    前端测试现状 经常听到后端同学说“单元测试”,前端写过测试用例的有多少?答案是:并不多,为什么呢?两个主要原因 1.前端属于GUI软件,浏览器众多,兼容问题让人头大,用户量有一定规模的浏览器包括: I ...

  7. 【Python + ATX基于uiautomator2】之编写unittest自动化测试脚本

    不说废话上代码: #!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2018/08/31 09:43 # @Author : zc # @ ...

  8. 【Python + ATX】之uiautomator2 PageObject模式自动化框架学习

    参考文章: 感谢:cynic (linpengcheng) <ATX 基于 ATX-Server 的 UI 自动化测试框架> <ATX-uiautomator2 实现 webview ...

  9. 【ATX学习大纲】【ATX基于uiautomator2+Python学习】之Android自动化

    github学习地址:https://github.com/openatx/uiautomator2 <_io.TextIOWrapper name='<stderr>' mode= ...

随机推荐

  1. 使用 Postman 实现 API 自动化测试

    背景介绍 相信大部分开发人员和测试人员对 postman 都十分熟悉,对于开发人员和测试人员而言,使用 postman 来编写和保存测试用例会是一种比较方便和熟悉的方式.但 postman 本身是一个 ...

  2. MongoDB 安全认证

    每日一句 Sometimes your whole life boils down to one insane move. 人一生中出人头地的机会不多,一旦有了一定要抓住! 概述 默认情况下,Mong ...

  3. Spring Ioc源码分析系列--Bean实例化过程(一)

    Spring Ioc源码分析系列--Bean实例化过程(一) 前言 上一篇文章Spring Ioc源码分析系列--Ioc容器注册BeanPostProcessor后置处理器以及事件消息处理已经完成了对 ...

  4. 《Unix 网络编程》15:Unix 域协议

    Unix 域协议 ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ 本 ...

  5. ExtJS配置TabPanel可以拖拽Tab标签页

    1.环境说明 ExtJS版本:7.4.0.42 Sencha Cmd: v7.5.1.20 开发工具:WebStorm 2022.1.1 PS:如果是老版本的ExtJS,引入Ext.ux.TabReo ...

  6. 【Java面试】为什么引入偏向锁、轻量级锁,介绍下升级流程

    Hi,我是Mic 一个工作了7年的粉丝来找我,他说最近被各种锁搞晕了. 比如,共享锁.排它锁.偏向锁.轻量级锁.自旋锁.重量级锁. 间隙锁.临键锁.意向锁.读写锁.乐观锁.悲观锁.表锁.行锁. 然后前 ...

  7. .Net Core 企业微信更新模版卡片消息

    1.搭建回调服务器 可参考:https://www.cnblogs.com/zspwf/p/16381643.html进行搭建 2.编写代码 2.1接口定义 应用可以发送模板卡片消息,发送之后可再通过 ...

  8. 运行时应用自我保护(RASP):应用安全的自我修养

    应用程序已经成为网络黑客想要渗透到企业内部的绝佳目标. 因为他们知道如果能发现并利用应用程序的漏洞,他们就有超过三分之一的机会成功入侵. 更重要的是,发现应用程序漏洞的可能性也很大. Contrast ...

  9. 安装ImageMagick7.1库以及php的Imagick扩展

    由于ImageMagick7以下不支持heic等图片格式,所以重新安装了ImageMagick7.1版本支持heic格式,并写此文章记录一下. 如果安装过程中遇到一些未知的错误,https://ima ...

  10. Spring框架系列(6) - Spring IOC实现原理详解之IOC体系结构设计

    在对IoC有了初步的认知后,我们开始对IOC的实现原理进行深入理解.本文将帮助你站在设计者的角度去看IOC最顶层的结构设计.@pdai Spring框架系列(6) - Spring IOC实现原理详解 ...