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

前言

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

一、PO模式简介

1.起源

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

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

2.PO六大原则

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

翻译成中文就是:

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

3.PO设计模式分析

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

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

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

5.业内常见的分层模型

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

1)四层模型

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

2)三层模型(推荐)

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

二、GUI自动化测试二三事

1.什么是自动化

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

2.为什么要做自动化

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

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

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

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

三、APP自动化测试实战

1.设计项目结构

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

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,从而更加简便地设计测试框架、组织用例等,但核心思想一直不变,都是为了实现代码逻辑和业务逻辑分离,从而达到灵活复用、以不变应万变的目的。

更多实战干货,欢迎扫码关注!

Original: https://www.cnblogs.com/dagangtest/p/16571357.html
Author: 大刚测试开发实战
Title: 基于UiAutomator2+PageObject模式开展APP自动化测试实战

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/602281/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 面试突击82:SpringBoot 中如何操作事务?

    在 Spring Boot 中操作事务有两种方式:编程式事务或声明式事务,接下来我们一起来看二者的具体实现。 1.编程式事务 在 Spring Boot 中实现编程式事务又有两种实…

    Python 2023年10月22日
    026
  • 基与python的GRPC SSL/TLS加密及Token鉴权

    SSL/TLS加密 1 // 创建grpc_ssl_key.pem和grpc_ssl_cert.pem 2 // 其中务必事先指定, 后续需要用到 3 openssl req -s…

    Python 2023年5月24日
    050
  • 进程间通信

    文章目录 进程间的通信 管道 * 匿名管道pipe – + * – 管道通信原理 – 代码层面原理: – 探索管道特性: &#821…

    Python 2023年10月27日
    027
  • 爬虫一些常用代码的记录

    写了一个GK2A卫星数据爬取的程序,本身不难,记录下小知识。 根据URL下载文件,有些需要cookie,大文件下载防止文件损坏 headers = { "Content-…

    Python 2023年10月31日
    030
  • 利用gromacs软件绘制自由能形貌图

    一、准备gromacs能够识别的文件: 模型的拓扑文件和坐标文件,在amber中,使用的后缀分别是.prmtop和.crd 1.利用python的parm模块 (要是装有amber…

    Python 2023年8月24日
    0100
  • DAY2 python基础

    shift向右缩进、ctrl+shift向左缩进 for…循环 范围 for i in range(10):print(“hello,I love this…

    Python 2023年5月24日
    084
  • matplotlib: AttributeError:‘DataFrame‘ object has no attribute ‘xx‘

    画图的时候,发现bug:读取data文件发现没有相应的属性。 问题在:csv文件的数据格式不对吧,正确格式应该为: “”,”Education&…

    Python 2023年8月18日
    047
  • 自从学会了用python解析视频,都不用去找下载按钮在哪了,是真的方便

    以前看个视频,可能还得到处找下载按钮,要不就有一些伪装的下载按钮,结果一点击,就弹出来的是广告,就很糟心… 但是 当有了Python之后,咱们只需要运行一下,分分钟就下…

    Python 2023年11月9日
    039
  • MindSpore尝鲜之Vmap功能

    技术背景 Vmap是一种在python里面经常提到的向量化运算的功能,比如之前大家常用的就是numba和jax中的向量化运算的接口。虽然numpy中也使用到了向量化的运算,比如计算…

    Python 2023年10月26日
    034
  • opencv-numpy操作

    在opencv中,调用numpy库,可以将图像以二维或三维数组表示,数组中的每一个值就是图像的像素值,所以可以通过numpy 对数组的操作,实现对图像的处理。 所以第一步,导入库 …

    Python 2023年8月28日
    059
  • Python遇见的BUG以及解决

    cannot import name ‘imread’ scipy库版本问题,安装较低的版本即可 No module named ‘numpy….

    Python 2023年8月29日
    041
  • ACM第二周—周赛—题目合集.

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言 A – k-LCM (easy version) B – Base K C…

    Python 2023年10月11日
    046
  • Django使用模板显示内容

    Django使用模板显示内容 原创 Jerrylee62022-08-03 10:03:19博主文章分类:DevOps ©著作权 文章标签 html django 数据 文章分类 …

    Python 2023年5月24日
    071
  • Pytest框架学习—环境准备

    Pytest介绍 pytest 是 python 的一种单元测试框架,与python 自带的 unittest测试框架类似,但是比unittest 框架使用起来更简洁,效率更高。根…

    Python 2023年9月14日
    044
  • Python 多重继承时metaclass conflict问题解决与原理探究

    最近有一个需求需要自定义一个多继承abc.ABC与django.contrib.admin.ModelAdmin两个父类的抽象子类,方便不同模块复用大部分代码,同时强制必须实现所有…

    Python 2023年10月17日
    061
  • carla入门

    一、安装 1.1 windows下安装 下载zip文件安装 到git下直接下载windows版本, 下面可以选择版本, 这里我用最新版 在解压的文件中的…\Python…

    Python 2023年9月17日
    089
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球