基于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/714638/

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

(0)

大家都在看

  • 剑指offer计划31(数学困难)—java

    1.1、题目1 剑指 Offer 14- II. 剪绳子 II 1.2、解法 刚刚好结束了,这个专题,国庆休息,后面再改 1.3、代码 class Solution { publi…

    技术杂谈 2023年7月25日
    094
  • 运算符重载限制

    p387 5.表 11.1 中的大多数运算符都可以通过成员或非成员函数进行重载,但下面的运算符只能通过成员函数进行重载。 =:赋值运算符。 ():函数调用运算符。 []:下标运算符…

    技术杂谈 2023年7月11日
    0107
  • 【原创】k8s微服务滚动发布(服务持续可用)实践笔记

    背景 对于业务和产品来讲,随时都有紧急小版本功能上线,对于研发人员来讲,线上如果有一些紧急的bug,都需要随时发版修正;而对于产品使用用户来讲,任何的功能和版本发布,要尽可能对用户…

    技术杂谈 2023年7月24日
    0109
  • Eureka

    一 什么是SpringCloud? Author:呆萌老师 QQ:2398779723 微信:it_daimeng Spring Cloud是&#x4…

    技术杂谈 2023年7月24日
    0118
  • Spine新手基础教程

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    技术杂谈 2023年5月31日
    0122
  • 【github项目】-CRM客户管理系统(基于SSM)

    CRM客户管理系统 基于SSM框架开发的CRM客户管理系统,适合刚学完SSM的同学,帮助夯实javase到ssm之间的知识,提升学生的逻辑思维,也了解到企业软件开发的流程及代码编写…

    技术杂谈 2023年7月10日
    0102
  • spring整合mybatis

    <dependencies> <dependency> <groupid>mysql</groupid> <artifacti…

    技术杂谈 2023年7月11日
    0116
  • 日本开放Wi-Fi 6E认证

    2022年9月2日,日本MIC(総務省)发布了”電波法施行規則等の一部を改正する省令(令和4年総務省令第59号)”省令,更新了Wi-Fi 6E Band 5…

    技术杂谈 2023年6月21日
    0118
  • 电脑双屏改单屏后看不到文件问题的解决

    之前电脑用的双屏幕,后来改为了单屏幕,发现之前放到另一屏幕上的文件双击打开后看不到,似乎还停留在另一屏幕的位置处。 解决的方法如下: 1.打开对应的文件(此时不要点击其他地方,确保…

    技术杂谈 2023年5月31日
    0217
  • CvMat 矩阵的使用方法和简单程序

    一:CvMat cvInitMatHeader( CvMat mat, int rows, int cols, int type,void* data=NULL, int step…

    技术杂谈 2023年5月30日
    090
  • 稀疏数组详细讲解

    稀疏数组的应用场景 稀疏sparsearray数组 稀疏:从字面意思理解就是为了压缩重复冗余的数据 基本介绍: 当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组…

    技术杂谈 2023年6月21日
    0104
  • MybatisPlus——全网配置最全的代码生成器

    MybatisPlus代码生成器 这里讲解的是新版 (mybatis-plus 3.5.1+版本),旧版不兼容 官方文档:https://baomidou.com/(建议多看看官方…

    技术杂谈 2023年7月11日
    091
  • 设计模式 21 状态模式

    状态模式(State Pattern)属于 行为型模式 在标准大气压下, 水在 0 ~ 100 度之间时,会呈现 液态;在 0 度以下会变成 固态;100 度以上会变成气态。 物质…

    技术杂谈 2023年7月25日
    0100
  • ORACLE数据恢复方法(提交事务也可以)

    今天在操作数据库的时候,发现数据操作错误,想要恢复,但是没有用事务,按理说,设置成不默认提交事务,此时所做的各种操作都没有反应到数据库中。这时,你可以rollback事务,撤销所有…

    技术杂谈 2023年5月31日
    099
  • 小熊飞桨练习册-08PaddleX底特律街景

    小熊飞桨练习册-08PaddleX底特律街景 简介 小熊飞桨练习册-08PaddleX底特律街景,是学习图像分割小项目,本项目开发和测试均在 Ubuntu 20.04 系统下进行。…

    技术杂谈 2023年7月23日
    0110
  • zabbix4.0本地安装详解及步骤

    安装前说明下,下面安装过程中涉及selinux部分仅供参考,可能会导致启动服务时产生各种报错,作者也是在折腾了无数日夜后报错不断而放弃治疗,直接永久关闭了selinux(啊,没有s…

    技术杂谈 2023年7月23日
    0104
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球