QT4i文档

QT4i(Quick Test for iOS),基于QTA提供面向iOS应用的UI测试自动化测试解决方案。

使用文档

使用前准备

QT4i依赖QTAF模块,使用前请参考《Testbase使用前准备》一节。

准备Mac电脑

QT4i的测试需要在Mac上执行,需要准备一台Mac并符合以下要求:

  • 操作系统为:Mac OS X EI Capitan(版本10.11.6及以上,推荐升级到最新版本)
  • 下载QT4i相关的工具
QT4i工具清单
工具名称 说明
UISpy 查看App的控件树以及控件QPath的工具,解压即可使用
QTA-IDE Python脚本开发环境(可选择自己喜欢的IDE)
Xcode QT4i依赖的xctest底层工具,Xcode6及以下不支持

准备iOS设备

QT4i至少需要一台iOS设备,可以是真机或模拟器。

使用iOS真机
  1. 通过USB连接iOS设备到Mac,初次连接时需要等待手机“是否信任此电脑”的弹框,点击“信任”。
  2. 查询iOS设备的设备标识符UDID
打开Xcode, 依次进入菜单Window->Devices and Simulators,查询获取iPhone手机的UDID,如下图所示:
_images/get_iphone_udid.png
  1. 打开UI Automation功能
_images/enable_uia_1.png _images/enable_uia_2.png
使用iOS模拟器
  1. 启动iOS模拟器(没有的话,可以新建)
  2. 打开UI Automation功能,方法同真机的方式。
调试内嵌WebView应用

如果使用iOS设备调试WebView页面,需要打开Safari浏览器的Web检查器。如下图所示:

  1. 选择“设置 => Safari浏览器 => 高级” :
_images/enable_web_1.png
  1. 允许Web Inspector服务 :
_images/enable_web_2.png

快速入门

测试项目

测试用例归属于一个测试项目,在设计测试用例之前,如果没有测试项目,请先参考《Testbase创建和修改测试项目》。按照该指引你可以创建一个例如名称为”demo”的项目。

安装QT4i

qt4i的正常使用依赖《fbsimctl》工具,首先需要安装fbsimctl:

$ brew tap facebook/fb

$ brew install fbsimctl --HEAD

然后安装qt4i:

$ pip install qt4i

部署XCTest Driver运行环境

  1. 调用测试项目的manage.py命令,解压并自动打开XCTestAgent的工程:
$ python manage.py qt4i.setup

如果没有创建测试项目,也可以执行qt4i-manage命令代理上述命令:

$ qt4i-manage setup

注解

XCTestAgent工程默认存放路径为 “~/XCTestAgent” 。

  1. 选择“Build Settings”>“Code Signing”一栏。
_images/xctest_2.png

全部修改为“Automatic”>“iOS Developer”,当成功添加了Apple ID后,签名会自动变为该ID对应的签名证书。

_images/xctest_3.png
  1. 选择“Build Settings”>“Packaging”一栏,修改“Product Bundle Identifier”的值,要求bundle id不能重复,建议将自己的名字作为后缀以区分(【注意】:不要出现下划线 )。
_images/xctest_4.png
  1. 选择“Build Settings”>“General”一栏, 勾选“Automatically manage signing”,此时应该没有任何错误提示(如果第3步中,后缀名出线下划线,会提示第二张图的错误)。
_images/xctest_5.png _images/xctest_5_error.png
  1. 选择“Product”>“Test”,构建及运行XCTestAgent。
_images/xctest_6.png
  1. 如果出现如下弹框,显示签名失败,选择“Fix issue”,然后添加一个Apple ID账户,选择该账户作为个人开发团队。
_images/xctest_7.png _images/xctest_8.png _images/xctest_9.png
  1. 如果出现如下弹框,需要在iPhone上设置该账户为信任开发者。
_images/xctest_10.png

进入iPhone:“通用”>“设备管理”>“开发商应用”,信任该账户开发者。

_images/xctest_11.png
  1. 如果出现如下弹框,说明bundle id不匹配,将手机上的app删除,再重新运行test。
_images/xctest_12.png
  1. 当Xcode test成功运行XCTestAgent,注意Xcode打印的信息,是否出现下图中红框内容,确保XCTestAgent在后台运行;否则需要重启手机,重新运行Xcode test。
_images/xctest_13.png
  1. 确保Xcode test能正常运行XCTestAgent后,停止运行test,并关闭Xcode。

理解UI结构

iOS的UI结构涉及三个概念,应用(App)、窗口(Window)、控件(Control):

  • App: 每一个iOS应用是一个App,即安装到手机上的待测应用,App基类由 qt4i.app.App 实现。
  • Window: 一个App包含多个Window,Window表示一个被测的App窗口界面,我们需要对每个窗口进行定义,以测试目标窗口,窗口基类由 qt4i.icontrols.Window 实现 。
  • Control: 一个Window包含多个Control,即每一个窗口包含了多个控件元素,QT4i中用QPath来查找定位控件,以便对各个控件进行操作。 控件基类由 qt4i.icontrols.Element 实现

所以我们需要以QT4i中的实现为基础,针对三种UI元素分别进行封装使用。

第一个测试用例

至此,运行qt4i所必要的环境,已经搭建完成,也对UI结构也有了一定的理解。

不妨以iOS系统自带的设置应用为例(模拟器和真机都有),开始写QT4i的第一个用例。

请先参考《Testbase设计测试用例》熟悉用例基本结构。由开头可知,我们已经创建了一个名为demo的项目,其中,

  • demotest:测试用例集合,这里存储所有测试用例的脚本。
  • demolib:测试业务库,这里存放所有测试业务lib层的代码,使得不同用例可以复用demolib的接口。
  • settings.py:项目配置文件,可以配置你所需要的项。

demotest目录下会自动生成hello.py文件,我们在该文件实现第一个简单的测试用例:

from qt4i.device import Device
from demolib.demotestbase import DemoTestcase
from demolib.demoapp import DemoApp


class DemoTest(DemoTestcase):
    'Demo测试'

    owner = 'apple'  # ----------------------------------------# 脚本的设计者
    priority = DemoTestcase.EnumPriority.Normal  # -----------------# 脚本的优先级
    status = DemoTestcase.EnumStatus.Design  # ---------------------# 脚本的状态
    timeout = 5  # -------------------------------------------------# 脚本执行超时值 (分钟)

    def run_test(self):
        self.start_step('1、申请资源启动DemoApp')
        device = Device()  # ---------------------------------------# 申请测试设备
        demoapp = DemoApp(device)  # -------------------------------# 启动被测试APP

        self.start_step('2、查询设备信息')
        isEnter = demoapp.enter()  # -------------------------------# 进入设备信息页面
        self.assertEqual("检验是否查看成功", isEnter, True) # --------# 检查是否进入成功

        self.start_step('3、修改设备名称')
        name = 'qt4i'
        isModify = demoapp.rename(name)  # -------------------------# 修改设备名称
        self.assertEqual("检验是否修改成功", isModify, True)  # ------# 检查是否修改成功

警告

在测试用例中强烈建议只调用lib封装的接口和断言操作,以保证App UI变化用例的逻辑不需要改动,同时也最大限度复用lib封装。

继承于测试基类

首先,用例继承于DemoTestBase测试基类,针对测试基类的封装可以参考《封装测试基类》一节。然后在run_test接口中编写你的测试用例。

获取设备

获取连接在Mac上的终端设备,利用 qt4i.device.Device 类实例化指定设备对象。如果未指定设备则获取链接在Mac上的第一个真机,如果没有真机,则会获取已经启动的模拟器:

device = Device(udid=None)     # 传入设备udid可以指定使用具体设备

警告

如果没有连接真机和已经启动的模拟器,则会抛出异常。

实例化App类

申请到设备后,开始实例化你的应用App类,针对DemoApp的封装可以参考《封装App》一节:

demoapp = DemoApp(device)
业务逻辑操作

在用例中只调用业务逻辑接口,而业务逻辑实现在demolib库下的各个文件中。本测试用例是验证修改设备名称是否成功,故此处调用:

demoapp.enter()    # -------------------------------# 进入设备信息页面
demoapp.rename(name)      # -------------------------# 修改设备名称

该功能会自动进入设备信息页面,并且修改设备名称。所以接下来验证测试功能点,设备名称是否修改成功:

self.assertEqual("检验是否修改成功",isModify,True) #------# 检查是否修改成功

至此,一个基本的用例就完成了。

UI元素封装

iOS窗口中的大部分控件元素需要用QPath进行定位。

封装测试基类

测试基类概述

QTAF中实现的测试基类《TestCase》提供了很多功能接口,如环境准备和清理、断言、日志相关等功能,详细见测试基类的相关说明。QT4i中的测试基类iTestBase重载了QTAF提供的测试基类,复用其功能,并扩展iOS需要的特定功能,如截图,获取crash日志等。

测试基类封装

目前qt4i的测试基类 qt4i.itestbase.iTestBase 已经实现了iOS需要的常用功能。你可以在demolib/demotestbase.py中封装你的测试基类DemoTestBase,并且该类继承于iTestBase,即可使用iTestBase中已有功能,同时可重载各个接口扩展针对你测试项目的自定义的功能。例如可如下使用:

from qt4i.itestcase import iTestCase
from qt4i.device    import Device
from testbase.conf import settings


class DemoTestcase(iTestCase):
    '''Demo测试用例基类
    '''

    def pre_test(self):
        '''初始化测试用例
        '''
        super(iTestCase, self).pre_test()
        self.log_info("%s.pre_test "%self.__class__.__name__)

    def post_test(self):
        '''清理下测试用例
        '''
        super(iTestCase, self).post_test()
        self.clean_login()
        self.log_info("%s.post_test "%self.__class__.__name__)

    def clean_login(self):
        '''清理App的登录状态
        '''
        for device in Device.Devices:
            device.remove_file(settings.APP_BUNDLE_ID, "/Documents/contents/DemoAccountManager")   #被测App登录态文件的存储路径

即可实现测试用例的环境准备或环境清理功能。除了以上封装的基本功能,你可能还需使用或重载其他接口,如:

  • ::根据进程名(可通过xcode查看),获取App的crash日志:

    def get_crash_log(self, procname):
    
  • 每个步骤前自定义一些操作,例如每个步骤前都打印出时间戳,看出每个步骤耗时等,可以重载下面接口:

    def start_step(self, step):
    

等等,更多参考QTAF和QT4i接口文档。

警告

重载基类各个接口时,必须显式调用基类的函数,以免基类的逻辑无法被执行到。

测试基类使用

在用例中将该类作为测试用例的基类:

class HelloTest(DemoTestBase):

封装App

App类概述

在demolib/demoapp.py中封装你的应用App类DemoApp,实现App类的基本功能 qt4i.app.App 类提供了常见功能,如启动App, 弹窗处理等。

App类封装

我们仍以Demo App为例,完整代码见Demo工程。被测应用的基本App类继承于qt4i.icontrols.App类,只需实现最基本的功能,如下:

from qt4i.icontrols import App
from testbase.conf import settings
from demolib.infowin import InfoWin
from demolib.namewin import NameWin


class DemoApp(App):
    '''DemoApp 负责被测应用的启动和初始化
    '''

    def __init__(self, device, app_name=settings.APP_BUNDLE_ID, trace_template=None, trace_output=None):
        '''APP应用(启动APP)

        :param device         : Device的实例对象
        :type device          : Device
        :param app_name       : APP的BundleID(例如:com.tencent.demo)
        :type app_name        : str
        :param trace_template : trace模板(专项测试使用,功能测试默认为None即可)
        :type trace_template  : str
        :param trace_output   : teace存储路径(专项测试使用,功能测试默认为None即可)
        :type trace_output    : str
        '''

        App.__init__(self, device, app_name, trace_template, trace_output)
        self.set_environment()
        self.start()

    def set_environment(self):
        '''初始化自动处理Alert弹框应对规则

        :param: none
        :returns: none
        '''
        # 此规则用于处理预期内容但难以预期弹出时机的Alert框(注意国际化多国语言的情况)。
        # 配置策略后,只要Alert框命中策略,即按策略处理。例如指定点击取消或确定按钮。
        self.rules_of_alert_auto_handle = [

            # 推送通知
            {
                'message_text': '推送通知|Notifications',  # 支持正则表达式
                'button_text': '^好$|^Allow$|^允许$'  # 支持正则表达式
            },

        ]

        # 此开关打开,用于处理不可预期内容且不可预期时机的Alert框
        # 如果Alert框命中上方的策略,则此项配置将被跳过。
        self.flag_alert_auto_handled = False

    def enter(self):
        '''进入设备信息函数
        '''
        infoWin = InfoWin(self)
        return infoWin.enter_info()

    def rename(self, name):
        '''重命名函数
        '''
        nameWin = NameWin(self)
        return nameWin.modify_name(name)

上述代码实现基本的App功能。主要包括App使用过程中,出现系统弹窗的自动处理。

App类使用

在用例中申请完设备后,即可开始实例化被测App,如下:

app = DemoApp(device)

封装QPath

使用场景

控件的定位和查找封装在lib包中的xxx_win.py中,建议App一个界面及该界面涉及的操作封装在一个.py文件中,封装的类需要直接继承于标准Window基类。

以封装系统设置App的“通用”控件为例:

self.updateLocator('通用': {'type': Element, 'root': self,
                       'locator': QPath("/classname = 'Cell' & label = '通用' & maxdepth = 7")}),
  • type: 指定控件的类型,和ios中定义的控件类型相对应,如TableView, Slider, ActionSheet等。
  • root: 指定控件的父节点,指定父节点后,查找控件时,会先找到父节点,然后以父节点为根节点,从根节点开始查找目标控件,这对于你定义的QPath找到多个的情况非常有用,如果指定了父节点,就只会返回父节点下的节点,否则找到多个重复节点就会报错。 如果指定为self,则表示会从整颗控件树根节点开始查找目标控件。
  • locator: 指定QPath

QPath语法的学习可参考《QPath语法和使用》。qt4i通过QPath定位控件,通过UISpy工具抓取控件树并获取指定控件的QPath。

警告

查看控件树,需要先部署XCTest Driver运行环境,具体可参考《部署XCTest Driver运行环境

使用UISpy即可获取设备当前页面控件树:

_images/uispy_xctest.png

注解

若遇到UISpy版本和QT4i版本不匹配的情况(RPC接口找不到),可以执行以下命令清理python环境:

$ killall -9 python

在上述第一个用例中提及的InfoWin界面封装:

from qt4i.icontrols import Window
from qt4i.icontrols import Element
from qt4i.qpath import QPath


class InfoWin(Window):
 '''DemoApp 输入登录页面
 '''

 def __init__(self, app):
     Window.__init__(self, app)
     self._device = self._app.device

     locators = {
         '通用': {'type': Element, 'root': self,
                   'locator': QPath("/classname = 'Cell' & label = '通用' & maxdepth = 7")},
         '关于本机': {'type': Element, 'root': self,
                      'locator': QPath("/classname = 'StaticText' & label = '关于本机' & visible = true & maxdepth = 12")},
         '名称': {'type': Element, 'root': self,
                  'locator': QPath("/classname = 'StaticText' & label = '名称' & visible = true & maxdepth = 11")},

         # "Table": {"type": Element, "root": self,
         #           "locator": QPath("/classname = 'Table' & visible = true & maxdepth = 10")},
         # "名称": {"type": Element, "root": "@Table",
         #           "locator": QPath("/classname='StaticText' & label ='名称' & maxdepth = 1")},

     }

     self.updateLocator(locators)

 def enter_info(self):
     '''账号密码输入登录函数
     '''
     self.Controls['通用'].click()
     self.Controls['关于本机'].click()
     return self.Controls['名称'].exist()

以用例中的‘名称’为例,UISpy查看和获取控件的QPath:

_images/uispy_name.png

控件的QPath定位可以基于其绝对路径获取(基于Window查找),也可以基于相对的路径获取。如果上图‘名称’这个控件基于Table查找,通过相对路径获取的话,QPath也可以写成这样:

"Table": {"type": Element, "root": self,
          "locator": QPath("/classname = 'Table' & visible = true & maxdepth = 10")},
"名称": {"type": Element, "root": "@Table",
          "locator": QPath("/classname='StaticText' & label ='名称' & maxdepth = 1")},

QPath中除了控件路径来限制定位符之外,还可以通过name、label等属性来进一步精确定位,其中label能够进行模糊匹配,使用“label ~= *”。

注解

基于界面的操作和判断都应该封装在lib包的xxxwin.py中,以确保在用例中只调用相关接口然后对返回值进行断言,这样有利于用例的维护和改造。同时封装基本界面时应该考虑通用接口的复用性。

另外,如果控件具有name属性(唯一),可以直接使用name作为QPath。

注意

定位符是QPath的必须指定最大查找深度,否则只会在Window下做第一级查找,只用name属性的定位符默认查找深度为20;如果相对路径查找,maxdepth指从父控件开始的查找深度。

在上述第一个用例中提及的NameWin界面封装:

from qt4i.icontrols import Window
from qt4i.icontrols import Element
from qt4i.qpath import QPath


class NameWin(Window):
    '''DemoApp 注销登录页面
    '''

    def __init__(self, app):
        Window.__init__(self, app)
        self._device = self._app.device

        locators = {
              '更多信息': {'type': Element, 'root': self,
                      'locator': QPath("/classname = 'Button' & label = '更多信息' & visible = true & maxdepth = 11")},
              '输入框': {'type': Element, 'root': self,
                     'locator': QPath("/classname = 'TextField' & visible = true & maxdepth = 11")},
              '名称值': {'type': Element, 'root': self,
                      'locator': 'qt4i'},
        }

        self.updateLocator(locators)

    def modify_name(self, name):
        '''
        '''
        self.Controls['更多信息'].click()
        name_text_field = self.Controls['输入框']
        name_text_field.click()
        name_text_field.value = name
        name_text_field.send_keys('\n')
        return self.Controls['名称值'].exist()

以用例中的‘名称值’为例,UISpy查看和获取控件的QPath:

_images/uispy_value.png
理解UI结构

iOS UI结构最外层是Window,然后是控件逐层嵌套,要访问指定控件,需要给出其唯一路径或者是唯一属性,如果给出QPath能指代多个控件,则会返回第一个实例。可以理解为控件是一个树状结构,从顶部逐级往下查找,给的定位符越明确、唯一越能帮助减少查找时间和查找错误:

_images/uispy_ui.png

定位符:

QPath("/classname='UIAWindow'/classname='UIACollectionView'/classname='UIACollectionCell' & instance=1")

上述QPath则会返回图中1号CollectionCell控件,如果要返回2号和3号CollectionCell控件,则分别需要用以下QPath:

QPath("/classname='UIAWindow'/classname='UIACollectionView'/classname='UIACollectionCell' & instance=2") #...Q2
QPath("/classname='UIAWindow'/classname='UIACollectionView'/classname='UIACollectionCell' & instance=3")  #...Q3

注解

instance用于限定其左边路径所指对象是第几个是实例,所以上面Q2中会查询到UIACollectionCell层取其第二个实例化对象,Q3依次类推。

控件类型和属性
控件类型

QT4i提供Element、Alert、Slider、ActionSheet和TableView共五种控件类型:

  • Element为通用控件类型,Button/Element/Image/ToolBar……以及上述未提及的控件类型;
  • Alert用于实例化iOS Alert弹窗控件,具有title和buttons属性,返回标题和所有子按钮;
  • Slider用于实例化iOS SliderBar控件,具有value属性,读取和设置滑动块位置;
  • ActionSheet用于实例化iOS ActionSheet控件,具有buttons属性,返回所有子按钮;
  • TableView用于实例化iOS TableView控件,具有cells属性,返回所有子列表。
控件属性

通常控件具有label、name、value、visible等属性,可以通过UISpy查看其属性值,Element对象可直接获取上述属性值。如果是可输入型控件入TextView,还可以通过赋值操作来改变其value值。

_images/uispy_attrs.png

更多关于控件属性和接口,请参考接口文档。

常见设备操作

在iOS自动化过程中,避免不了的是对设备的各种操作,如将当前App退到后台、滑动窗口、屏幕点击、音量调节等,现针对常见的设备操作进行解析,更多的功能请参考接口文档。

点击屏幕

如果你想直接基于屏幕进行点击操作,可以直接调用 qt4i.device.Device 中定义的click()方法:

device = Device()
device.click()

默认点击屏幕正中间。

警告

通常情况下请优先使用QT4i各个控件类型提供的click接口去点击,只有在特殊情况下不方便使用该接口才改为点击屏幕固定坐标的方式。

模拟按键

iOS设备上有很多虚拟按键,如HOME键、音量键等,QT4A封装了常见的按键,在用例中实例化App类后可以获得app对象:

device = Device()

然后可以模拟发送各类按键,如home键

device.deactivate_app_for_duration(seconds=-1)   #seconds为-1时,则模拟按Home键的效果; seconds传入整数值,可以将当前App置于后台一定时间

重启键:

device.reboot() #重启

锁屏和解屏键:

device.lock()   #锁屏
device.unlock()  #解屏

音量调节(模拟器没有):

device._volume('up')  #调高音量

另外, qt4i.device.Device 类封装的接口,还供siri交互,旋转屏幕等接口。

滑动屏幕

有时候你需要针对屏幕进行滑动,例如若App类开头有一些广告页面,需要滑动才会消失,那么可以调用:

def drag(self, from_x=0.9, from_y=0.5, to_x=0.1, to_y=0.5, duration=0.5):
    '''回避屏幕边缘,全屏拖拽(默认在屏幕中央从右向左拖拽)

    :param from_x: 起点 x偏移百分比(从左至右为0.0至1.0)
    :type from_x: float
    :param from_y: 起点 y偏移百分比(从上至下为0.0至1.0)
    :type from_y: float
    :param to_x: 终点 x偏移百分比(从左至右为0.0至1.0)
    :type to_x: float
    :param to_y: 终点 y偏移百分比(从上至下为0.0至1.0)
    :type to_y: float
    :param duration: 持续时间(秒)
    :type duration: float
    '''

传入不同的坐标值,便可以实现上下左右,不同幅度的滑动。

屏幕截图

在执行用例过程中,有些场景需要截图下来帮助分析,可以调用接口:

device = Device()
device.screenshot(image_path='/User')

当然,QT4i在用例失败时也会截图保存App现场。如你还需其他截图,可自行调用。

进阶指南

介绍QT4i的高级特性和使用方法。

弹窗的自动处理

QT4i提供了弹窗的自动处理机制,当被测App在用例执行中出现Alert弹窗时,可以通过在被测App的测试基类中 定义弹窗的处理规则来实现自动处理。

弹窗处理规则说明:

  1. 优先遍历用户定义的预期规则 rules_of_alert_auto_handle 处理(其中 message_text 表示Alert标题栏文字, button_text 表示Alert的按钮字段,两者均支持正则匹配);
  2. 如果1中定义的规则不满足,则按照flag_alert_auto_handled设置的规则处理;
  3. rules_of_alert_auto_handle 设置为True,但是又希望某个弹窗由用例中来处理,则可通过在 rules_of_alert_auto_handle 中添加只包含message_text的规则实现。

使用示例如下:

class DemoApp(App):
    '''被测App基类
    '''

    def __init__(self, device, app_name=settings.APP_BUNDLE_ID, trace_template=None, trace_output=None):
        App.__init__(self, device, app_name, trace_template, trace_output)
        self.set_environment()
        self.start()

    def set_environment(self):
        '''配置自动处理Alert弹框规则
        '''
        self.rules_of_alert_auto_handle = [

            # 测试账号
            {
                'message_text' : '测试号码',  # 支持正则表达式
                'button_text'  : '^确定$'  # 支持正则表达式
            },

            # 退出登录
            {
                'message_text': '退出登录', #屏蔽退出登录的自动处理
            },
        ]
        self.flag_alert_auto_handled = False

注解

如果弹窗被处理了,但是预期的控件出现找不到的场景,可适当延长该控件的查找时间即可。

多终端特性

多终端特性是指在同一个测试用例中允许申请多台iOS设备进行测试和操作,实现多台iOS设备之间的协作测试。

应用场景

多终端特性适用于多台设备之间协作完成的测试场景,例如:iPhoneQQ音频通话、视频通话;来电的实时通话等。

_images/multi_demo.png
使用方法

在测试用例创建一个新的Device对象,即申请一台终端设备,创建多个Device对象,即申请了多台终端设备。默认情况下, 申请的第一台设备为本地连接的设备,后续其他设备为远端的协作设备(协作设备的配置方法参考下面章节)。

from qt4i.device import Device

device = Device() #----------------------------------------# 申请第一台测试设备
device2 = Device() #----------------------------------------# 申请第二台测试设备
协作设备的资源配置

协作设备的资源配置方法按照用例执行模式的不同,分为本地调试模式和QTA自动化测试平台执行模式,具体参考如下章节。

本地调试用例
  1. 在协作Mac机器上启动Driver: 运行工程目录中manage.py脚本,命令如下:
python manage.py qt4i.restartdriver -t xctest
  1. 在本地Mac机器上的工程目录中settings.py文件中配置步骤1中的协作机(Mac电脑)的IP地址,如下所示,即可在本地调试多终端的用例。
QT4I_REMOTE_DRIVERS = [{'ip':'10.68.64.128'},]

内嵌webview的自动化

针对内嵌webview的iOS App, QT4i提供了两种UI自动化测试方案:基于原生控件树和基于H5页面DOM树的测试方案。

基于原生控件树的测试方案

基于原生控件树的测试方案也即把webview当做原生控件,按照QT4i的UI元素定义和封装,即可完成UI自动化测试,具体 可参考UI元素封装 UI元素封装

基于H5页面DOM树的测试方案

注解

开始本节学习前,请熟悉 qt4w 基础知识,同时参考《调试内嵌WebView应用》一节准备好iOS设备环境。另外,qt4i提供一个针对内嵌webview的iOS App做自动化的《demo》示例,可供参考。

基于原生控件树的测试方案在某些测试场景中可能存在一些不足,例如:控件ID缺失不方便定位控件,H5页面映射到原生控 件树导致一些控件元素丢失等等。因此,我们提出了基于H5页面DOM树的测试方案,该方案基于H5页面的DOM树进行UI控件 的查找定位,不依赖原生控件树,可以有效地解决原生控件树的问题。该测试方案的具体使用步骤如下:

  • 通过操作原生控件,进入H5页面
  • 实例化IOSWebView
  • 封装WebPage
  • 通过WebPage进行Web控件的查找和操作
安装QT4W

在安装了QT4i后,如果需要进行Web自动化测试,还需要通过pip安装的方式安装QT4W:

pip install qt4w
IOSWebView的定义

qt4i.web.IOSWebView 是QT4i实现的WebView类,提供iOS端的web控件的接口实现,包含获取webview坐标, web控件的点击、滑动、长按、文本输入以及js注入等功能。用户需要通过原生控件的方式定义IOSWebView:

from qt4i.icontrols import Window

class BrowserWindow(Window):
    '''
    浏览器窗口基类
    '''


    def __init__(self, app):
        self._app = app
        Window.__init__(self, self._app)
        scroll_win = Window(self._app, "webroot")
        locators = {
            'webview' : {'type':IOSWebView, 'root':scroll_win, 'locator':'webview', 'url':'index', 'title':'demo'},
        }
        self.updateLocator(locators)

其中title和url是可选参数,默认都不提供的话,从WebInspector的缓存中获取第一个H5页面。

  • title: 对应H5页面的 document.title 的内容
  • url: 对应H5页面的 location.href 的内容, 支持正则表达式
WebPage的封装

一个WebPage对应一个H5页面,通常由一个 ui_map 字典组成,其中包含了对当前H5页面中的若干 web控件(WebElement)的定义,每个web控件的定义包含控件名、控件类型、locator、ui_map等属性。

web控件的属性详解
属性名 必选项 描述
控件名 Y 包含对web控件的文本描述,对应于字典中的key
type N 控件类型,默认值为WebElement,如果需要定义成数组,需要使用ui_list(xxx)
locator Y Web控件定位符,使用XPath描述
ui_map N 定义当前Web控件的子控件的属性字典,可以嵌套,内容可以包含以上所有属性,包括”ui_map”

注解

web控件的XPath可以通过苹果官方的工具 Safari Technology Preview 查看

下面的代码片段展示了一个WebPage的封装:

from qt4w import XPath
from qt4w.webcontrols import WebElement, WebPage, ui_list

class LifePrivilegePage(WebPage):
    '''生活特权页面
    '''
    ui_map = {
        '限时福利列表': {
            'type': ui_list(WebElement),
            'locator': XPath('//div[@class="mod-list list-walfare"]/ul/li'),
            'ui_map':{
                '名称': XPath('//div[@class="info"]/h3'),
                '描述': XPath('//div[@class="info"]/p[1]'),
                '我要抢': XPath('//p[@class="surplus-time"]/button')
            }
        }
    }

注解

web控件的XPath可以通过苹果官方的工具 Safari Technology Preview 查看

web控件的查找和操作

在介绍完成webview定义和WebPage封装之后,接下来就给大家讲解如何使用WebPage进行Web控件的查找和操作。

  1. 首先进入App的H5页面
  2. 初始化webview
  3. 使用步骤2中的webview初始化WebPage
  4. 使用control(‘控件名’)的方式查找web控件
  5. 基于WebElement提供的接口获取web控件的属性和对web控件进行点击等操作

下面的代码片段展示了Web控件的查找和操作的具体步骤:

device = Device()
app = DemoApp()
app.enter_h5_page()                               #  进入H5页面
webview = BrowserWindow(app).Controls['webview']  #  初始化webview
page = LifePrivilegePage(webview)                 #  初始化WebPage
for elem in page.control('限时福利列表'):           #  查找'限时福利列表'
    name = elem.control('名称').inner_text
    description = elem.control('描述').inner_text

WebPage和WebElement类提供了诸多Web相关接口(例如 :inner_text)可参考《qt4w》的文档。

App自动化可测性提升

什么是可测性

软件测试中的可测性(testability)一般是指对系统的可控性、可观察性、可隔离性以及稳定性等进行的衡量评估,借以反映系统设计、实现 对测试的友好程度和相应的测试成本。具体到iOS App的UI自动化测试中,主要体现在iOS App自身行为的稳定性以及控件的可识别性上。下面 将从这两个维度探讨如何提升iOS App的可测性。

如何提升iOS App的可测性

俗话说,巧妇难为无米之炊。UI自动化测试工作也是同样道理,QT4i为我们提供了简单易用的平台工具,但是作为被测对象的iOS App 本身并不具备可测性,那我们也很难顺利开展自动化测试工作,所以,提升iOS App的可测性显得尤为重要。

排除iOS App的外部干扰,提升自身的稳定性

iOS App作为UI自动化的被测对象,自身行为的稳定性直接决定了UI自动化效果的好坏。所以,UI自动化需要聚焦App的功能特性,尽量 排除外部因素的干扰。下面介绍一些常用的提升iOS App自动化稳定性的方法。

1. 屏蔽App交互中的干扰行为。例如:iOS App使用测试帐号时,后台策略会在交互过程中额外出现测试弹框提醒(该类弹框并非业 务本身的功能特性,而且用例脚本中自动处理这类弹框不稳定),针对这类弹框,最有效的手段是通过wtlogin后台配置策略,屏蔽 掉被测App中的此类弹框;对于iOS App启动时的推送通知授权弹框,则可以考虑在客户端代码中屏蔽掉,有效避免因推送通知授权弹框 的出现过早导致的测试用例失败。

_images/test_alert_win.png
  1. 初始化环境,保证测试环境的一致性。例如:清理测试帐号的登录态,保证每次启动App都是未登录的状态。

3. 统一预置条件,保证交互的一致性。例如:涉及选取文件的操作(发送文件或者上传图片等),由于文件生成本身不是测试点,可以考虑 在iOS测试机器上的图库中预置相同名称的文件夹和相同数量的照片作为测试资源(对于文件大小有要求的可以预置一些视频文件),以此保 证选取文件的UI操作的一致性和稳定性。

_images/qta_album.png
初探accessibility,提升控件的可识别性

对于iOS App的系统测试同学,大都知道VoiceOver阅读功能和 accessibility 属性之间的映射关系,但是UI自动化和accessibility 之间又有着怎样的千丝万缕的联系呢?下面我们一起走进accessibility的世界,探讨提升iOS App的控件可识别性的有效途径。

iOS accessibility的基本介绍

iOS accessibility的相关属性由两个常见的协议组成(UIAccessibility Protocol和UIAccessibilityIdentification Protocol), VoiceOver使用了UIAccessibility Protocol,UI自动化的测试框架UIAutomation则同时使用了两个协议,如下图所示。

_images/accessibility.png

这两个协议包含了accessibility很多属性,详见 苹果官方指南 , 这里我们重点介绍和UI自动化相关的四个属性:

  • accessibilityIdentifier(自动化的专属控件属性,对应UISpy中控件的name属性)
  • accessibilityLabel(VoiceOver和自动化公用的控件属性,对应UISpy中控件的label属性)
  • accessibilityValue(VoiceOver和自动化公用的控件属性,对应UISpy中控件的value属性)
  • accessibilityTraits(VoiceOver和自动化公用的控件属性,决定UISpy中控件的显示类型)

其中,accessibilityIdentifier是作为识别控件的首选属性,因为它既不会影响VoiceOver的阅读功能,也不会随着无障碍化需求 的变更发生变化(稳定性较好);accessibilityLabel直接对应VoiceOver的文本内容(随着无障碍化需求的变更而变化),可以作 为控件识别的补充属性;accessibilityValue通常用于存放动态的内容,则适于作为已知控件的合法性校验属性。accessibilityTraits 属性通常情况下不用单独设置,默认会关联上一个标准控件类型,对于自定义的控件,如果没有显式给出控件类型, 可以考虑 设置accessibilityTraits属性

iOS accessibility最佳实践

下面结合几个典型场景,给出提升控件的可识别性的最佳实践。

  • 控件类型不能唯一标识控件:

    QPath:  /classname=‘UIAWindow’/classname=‘UIAButton‘
    
_images/id_case_1.jpeg

【解决方法】iOS App代码中设置控件的accessibilityIdentifier属性(见下图的objective-c代码), 结合控件类型和控件name属性作为控件的QPath。

customView.accessibilityIdentifier = @"搜索";
设置后的QPath:  /classname=‘UIAWindow’/classname=‘UIAButton‘ & name=‘搜索’
  • 控件的label属性动态变化:

    QPath:
    /classname=‘UIAWindow’/classname=‘UIATableView’/classname=‘UIATableCell’ & label=‘开通会员’
    /classname=‘UIAWindow’/classname=‘UIATableView’/classname=‘UIATableCell’ & label=‘我的会员’
    /classname=‘UIAWindow’/classname=‘UIATableView’/classname=‘UIATableCell’ & label=‘我的超级会员'
    
_images/id_case_2.jpeg _images/id_case_3.jpeg

【解决方法】App代码中设置控件的accessibilityIdentifier属性(设置方法同上),用控件name属性替换label属性作为控件的QPath:

设置后的QPath:  /classname=‘UIAWindow’/classname=‘UIATableView’/classname=‘UIATableCell’ & name=‘会员中心’
  • 内嵌webview(H5页面)的控件如何识别

【解决方法】目前UI自动化提供了两种识别手段

  1. 映射为native控件的方式:有text标签的,accessibilityLabel自动继承text内容,无需单独设置;无text标签的(如input、image),需要通过添加aria-label属性即可。
<div id="fkbx-hspch" tabindex="0" aria-label="正在收听"></div>
  1. QT4W(正在开源中):设置webview页面的标题(确保唯一性),用于webview的识别。

综上,提升iOS控件的可识别性的最佳实践为:

  1. 优先设置iOS控件的accessibilityIdentifier属性(控件id),并将其作为QPath中的识别控件的首选属性;
  2. iOS App的UI主界面的入口控件或者通用控件的accessibilityIdentifier属性尽量固化下来,保持不变,例如: iPhoneQQ中的会员入口控件、AIO会话窗口的返回按钮等;
  3. 对于hybrid App, 优先考虑采用QT4W识别WebView,保证WebView的标题唯一性。
玩转iOS的黑魔法,揭秘iOS App控件id自动生成方案

在熟悉了accessibility的来龙去脉之后,我们知道如何灵活地给被测App加上控件id。但是问题来了,全部控件的属性是不是都需要 一个一个手工添加,开发表示时间紧迫,鸭梨山大。特别是对于一个全新的App,开发初期没有考虑这些需求,控件属性基本都为空白。 有没有一枚“银弹”,可以快速补全这些控件id呢?答案是肯定的,下面就来揭晓如何自动生成iOS的控件id。

剖析控件id自动生成原理

这里所说的iOS黑魔法就是Method Swizzling,它是利用Objective-C的运行时特性,改变或者扩展原有的函数功能,实现对原有代码的注入。 控件id自动生成正是利用这个特性,动态替换UIView类的accessibilityIdentifier方法,加入控件id的生成规则,最终实现自动添加控件控件id。 下面具体介绍Method Swizzling的操作步骤。

  • 实现指定函数的扩展功能(自由发挥)
- (void)swizzled_viewDidAppear:(BOOL)animated
{
    // 调用原方法
    [self swizzled_viewDidAppear:animated];  // 此处不是递归调用,在运行时会被替换成原方法
    // 扩展功能,此处仅打印日志
    NSLog(@"swizzled:%@",NSStringFromClass([self class]));
}
  • 替换原有函数(标准模板)
+ (void)swizzleMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel
{
    Method origMethod = class_getInstanceMethod(class, origSel);
    Method swizMethod = class_getInstanceMethod(class, swizSel);

    BOOL didAddMethod = class_addMethod(class, origSel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod));
    if (didAddMethod) {
        //原方法不存在,而是继承了父类的实现,则将父类的实现替换到swizMethod中,从而实现在swizMethod对父类方法的调用
        class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    } else {
        //原方法已存在,直接交换方法
        method_exchangeImplementations(origMethod, swizMethod);
    }
}
  • 寻找注入时机,加载新功能(load方法在类加载时会被自动调用一次)
+ (void)load
{
   SEL origSel = @selector(viewDidAppear:);
   SEL swizSel = @selector(swiz_viewDidAppear:);
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
     [UIViewController swizzleMethods:[self class] originalSelector:origSel swizzledSelector:swizSel];
   });
}

更多Method Swizzling请参考 这里

介绍控件id的生成规则

通常情况下,由于UIAutomation只关注控件树中的叶子节点对应的控件的可操作性以及属性,所以UIAutomation的UI控件树的层次结构 并不是严格对应代码中UI控件结构,而是代码中控件树的简化版。所以,如果让代码中的全部UI控件显示在UIAutomation的控件树中将会 导致控件树的过度庞大,影响UI控件的查找效率,如下图所示。

_images/auto_id_1.jpeg

因此,选择合适的id生成规则就显得尤为重要,具体规则按优先级排列如下:

  1. Id(accessibilityIdentifier)不为空,也即id内容在代码已经设置,直接返回该id即可;
  2. 若id为空,则选择UI控件实例的变量名作为控件id,具体获取方法如下(利用runtime的接口,遍历当前view的superview,获取当前view实例的变量名);
- (NSString *)getVarNameWithInstance:(UIView *) instance {
    unsigned int numIvars = 0;
    NSString *key=nil;
    Ivar * ivars = class_copyIvarList([self class], &numIvars);
    for(int i = 0; i < numIvars; i++) {
        Ivar thisIvar = ivars[i];
        const char *type = ivar_getTypeEncoding(thisIvar);
        NSString *stringType =  [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        if (![stringType hasPrefix:@"@"]) {  // 过滤掉非OC类型
            continue;
        }
        if ((object_getIvar(self, thisIvar) == instance)) {
            key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
            break;
        }
    }
    free(ivars);
    return key;
}

【说明】针对UIImageView,需要单独hook accessibilityLabel以实现accessibilityIdentifier属性的设置。

  1. 若步骤2中的UI控件的实例变量名为空时,可以枚举以下常用UI控件单独生成id内容:

1) UILabel:使用text作为id

2) UIButton:使用titleLabel.text作为id

3) UIImageView:使用image的文件名作为id(此处需要hook image的加载函数)

4) 其他app自定义的UI控件待补充

插桩特性

插桩特性是指通过对被测App注入代码,提高App可测性的一种方式。目前主流的插桩方法有以下两种:

  • 静态插桩:在已有目标App源码的前提下,加入实现的framework代码一起编译;
  • 动态插桩:在没有目标App源码的前提下,通过一些技术手段实现对目标app的ipa安装包的修改,再将修改后的app安装到手机设备上,从而改变目标app的表现行为。

这是一种侵入式自动化测试方案,使用合适,可以获得理想的效果。

目前,qt4i在 qt4i.device.Device 中提供通用桩的调用接口:

call_qt4i_stub(method, params, clazz=None)
    '''QT4i Stub通用接口

    :param method: 函数名
    :type method: str
    :param params: 函数参数
    :type params: list
    :param clazz: 类名
    :type clazz: str
    :return: 插桩接口返回值
    :rtype: str
    '''
应用场景

QT4i是基于Apple官方提供的XCTest框架实现的,受限于XCTest和被测app之间的通信是跨进程的方式,很多基于被测app内部信息的测试场景就无法覆盖。

而插桩的测试方案很好的弥补了XCTest的不足,因为插桩保证了测试进程和被测app是同一进程(即进程内),可以方便采集被测app的内部信息,依据测试需要修改被测app的状态等。

下面列举出几种插桩在qt4i自动化测试的典型应用:

  • app的沙盒访问:读取沙盒目录, 清除登录态文件;(对比利用itunes私有协议访问沙盒,插桩方案更可靠稳定,访问效率更高)
  • 访问系统相册:上传图片,比对图片;(由于系统相册中的图片iOS系统进行了加密,无法直接访问,此时插桩可以轻松实)
  • 获取自动化测试资源;(譬如自动化用例需要一些比较大的视频文件,相比于在mac上下载后,通过usb传输到手机,让手机上app主动下载是效率更好的方式,插桩便可以实现)
  • 动态修改当前窗口某控件的属性;
  • 读取当前窗口控件的详细数据;
如何扩展QT4i通用桩
_images/flow.png

上图是如何扩展QT4i通用桩,实现调用App进程内方法的流程图。主要分为3个步骤,下面以 qt4i.device.Device.download_file 中,基于通用桩接口实现的大文件下载方法为例,介绍如何扩展QT4i通用桩:

1 、扩展QT4iStub方法, 在QT4iStub工程目录下QT4iManager.m文件中实现如下方法:

- (BOOL) createFile:(NSString *)filename withPath:(NSString *)path size:(NSInteger)size
{
        .......实现文件下载逻辑.........
}

将扩展后的QT4iStub制作成framework,便可以开始插桩了。

2、对目标App进行插桩,按需分为静态插桩和动态插桩。

  • 静态插桩

将上述扩展后的framework直接加入App源码中,同时需要在app启动调用函数中加入以下注释提示的一行代码,用于启动插桩的server:

#import <QT4iStub/QT4iStub.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    ViewController * vc = [[ViewController alloc] init];
    .................................
    .................................
    [self.window makeKeyAndVisible];
    [[QT4iManager sharedManager] startExplorer];  //需要加入的代码
    return YES;
}

和App源码一起编译即可。

  • 动态插桩

由上面可知动态插桩,是针对没有源码的情况,通过工具生成dylib,动态注入到App内,并对app进行重签名的方式。主要需要经过以下流程:

_images/dylib.png
  • 加入上述扩展后的framework,制作dylib
  • 注入目标app,保证目标启动时会加载hook的动态库
  • 重签名目标app,保证修改后的app能在非越狱的手机上能安装并正常启动

在插桩原理中会对动态插桩做详细说明。

3、通过在用例中调用qt4i提供的通用桩调用接口(确保传入方法名和参数与测试桩保持一致),便可实现调用QT4i Stub扩展的方法:

def download_file(self, file_url, dst_path):
   '''从网上下载指定文件到本地

   :param file_url: 文件的url路径,支持http和https路径, 需要对app插桩
   :type file_url: str
   :param dst_path: 文件在手机上的存储路径,例如:/Documents
   :type dst_path: str
   '''
   method = 'createFile:withPath:size:'
   params = [file_url, dst_path, 1]
   self.call_qt4i_stub(method, params)

注解

QT4i Stub是可扩展的,只需按照上述流程,实现所需方法,对目标App进行插桩即可。调用时,需要注意传入对应方法名和参数。

插桩原理

最后,介绍QT4i通用插桩的实现原理,流程图如下:

_images/stub.png

QT4i框架设计自上而下是API层、driver层和device层:

针对插桩,在API层提供call_qt4i_stub方法,传入参数和方法名或类名,通过rpc通信发送给driver层,再经过端口转发,通过usb发送给iOS设备上(device层)拉起的XctestAgent。 在iOS设备上,XctestAgent作为client端,将信息发送给APP加载时启动的QT4iStub Server端,从而达到调用APP进程内的方法的目的。

这里,着重介绍下动态插桩原理,以上节扩展通用桩为例:

① 首先,通过工具《theos》或《MonkeyDev》创建iOS tweak工程,以theos为例:

_images/theos.png

注解

何为tweak? Tweak实质上是iOS平台的动态库,以dylib这种形式存在,类似Windows 下的 .dll,Linux 下的 .so。与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用,等到程序运行时,动态库才会被真正加载进来。

然后,将扩展的QT4iStub framework加入到上图生成的tweak工程中的,打开工程目录下的Tweak.xm文件,加入以下代码, 确保app启动时,启动插桩的server:

#import <QT4iStub/QT4iStub.h>
#import <objc/runtime.h>

__attribute__((constructor)) static void entry() {
    NSLog(@"QT4iManager starting...");
    [[QT4iManager sharedManager] startExplorer];
}

如果需要hook源码中的某个方法,可以使用theos提供的一套《Logos》命令,编写hook方法的具体内容。譬如,希望目标App退出后台的时候打印一条日志,可以在Tweak.xm文件中继续追加如下代码:

%hook AppDelegate

// Hooking an instance method with an argument.
- (void)applicationDidEnterBackground:(UIApplication *)application {
    NSLog(@"App entered background!!!");
    %orig; // Call through to the original function with its original arguments.
}
%end

只需直接在Tweak工程下面make一下,便可以生成所需的dylib文件的。

注解

何为Logos?在Logos命令背后,theos定义了一系列的宏和函数,底层利用objective-c的runtime特性来替换系统或者目标app的函数(Method Swizzling),从而实现对目标app的hook。

② 为了保证目标app运行时加载生成的dylib,必须保证app中存储有指向生成的dylib的引用,其实就是修改目标app的二进制文件。

在修改之前,先了解下iOS中Mach-O可执行文件的格式,如图:

_images/mach.png
  • Mach-O头部(mach header):描述了Mach-O的cpu架构、文件类型以及加载命令等信息。
  • 加载命令(load command):描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令表示。
  • Data: Data中的每个段(segment)的数据都保存在这里,段的概念与ELF文件中段的概念类似。每个段都有一个或多个Section,它们存放了具体的数据与代码。

了解了Mach-O文件格式后,便明白我们只需在目标app的可执行文件的Load Command部分添加一个加载命令LC_LOAD_DYLIB指向生成的dylib,便可以使目标app启动时可以加载生成的dylib,这里推荐使用《insert_dylib》工具实现。成功后,可以使用《machoview》工具查看如下:

_images/load.png

③ 最后使用证书对上述文件进行重签名就可以了,重签名工具很多,这里使用《ios-app-signer》操作步骤如下:

  • 使用codesign -fs +证书 + dylib命令,对生成dylib进行重签名;
  • 将重签名后的dylib放入目标App解压后的Payload/app名的目录下;
  • 使用重签名工具,对整个安装包使用有效的证书重签名;

警告

重签名证书一定要使用花钱买的个人开发者证书或者企业证书!

Q&A

记录QT4i使用中的常见问题及解决方案。

安装qt4i报错

安装qt4i时,会安装依赖pymobiledevice-qta,该包的安装会依赖于M2Crypto加密包。

部分MacOS在编译安装M2Crypto库时,可能会报如下两种错误:

_images/qa_0_1.png _images/qa_0_2.png

这是因为M2Crypto库编译安装时,依赖于系统C环境的路径和版本不匹配导致的,可以按照《M2crypto官网帮助文档》解决这个问题。

_images/qa_0_3.png

手机系统弹框

启动APP失败,报告中显示错误堆栈:

Fault: <Fault 1: <type 'exceptions.Exception'>:DeviceDisabled>

查看截图

_images/qa_1.png

解决方法:

人工点击按钮“好”,关闭弹框

网络问题

用例登录失败

_images/qa_2.png

解决方法:

检查手机WiFi连接是否正常,如果未连接网络,需要手动重连。

Mac解析hostname异常

运行出现以下错误:

_images/qa_3_1.png

解决方法:

  • 查看 系统偏好设置->共享->电脑名称,保证“电脑名称”与hostname一致
_images/qa_3_2.png _images/qa_3_3.png
  • 方法一:
  • 在终端运行如下命令:(其中newName.local替换成系统偏好设置->共享查到的如上图所示内容)
$ sudo scutil --set HostName newName.local
  • (方法一执行完错误仍在使用方法二)

  • 方法二:

  • 前往Macintosh HD -> Library/资源库 -> Preferences -> SystemConfiguration,删除以下文件:

    com.apple.airport.preferences.plist
    NetworkInterfaces.plist
    Preferences.plist
    
_images/qa_3_4.png
  • 重启计算机

端口被占用异常

本地调试用例时,出现Address already in use异常:

File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/rpc.py", line 41, in __call__
  return self.method.__get__(self.instance, self.owner)(*args, **kwargs)
File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/xctest/wda.py", line 95, in dismiss_alert
  self.agent.execute(Command.QTA_ALERT_DISMISS, env)
File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/xctest/wda.py", line 69, in __get__
  val = self.func(instance)
File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/xctest/wda.py", line 108, in agent
  return self.agent_manager.get_agent(self.udid)
File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/xctest/agent.py", line 125, in get_agent
  return self.start_agent(device_id)
File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/xctest/agent.py", line 91, in start_agent
  self._agents[device_id] = XCUITestAgent(device_id, server_ip, server_port, keep_alive, retry, timeout)
File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/xctest/agent.py", line 189, in __init__
  self.start(retry, timeout)
File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/xctest/agent.py", line 287, in start
  self._tcp_relay()
File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/xctest/agent.py", line 205, in _tcp_relay
  raise self._relay_error
error: [Errno 48] Address already in use

解决方法 :

①可通过命令查看8100端口被哪个进程占用:

$ lsof -i :8100

②根据进程PID ,kill掉占用进程:

$ kill -9 (进程PID)

③重新跑用例即可。

UISpy连接失败报错

打开UISpy,点击连接,报错:

File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/rpc.py", line 64, in _dispatch
  return m(*params)
File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/rpc.py", line 41, in __call__
  return self.method.__get__(self.instance, self.owner)(*args, **kwargs)
File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/host.py", line 65, in list_devices
  return DT().get_devices()
File "build\bdist.win32\egg\testbase\util.py", line 158, in __call__
File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/tools/dt.py", line 77, in __init__
  self.xcode_version = DT.get_xcode_version()
File "build/bdist.macosx-10.13-intel/egg/qt4i/driver/tools/dt.py", line 96, in get_xcode_version
  raise Exception('get_xcode_version error:%s' % e.output)
Exception: get_xcode_version error:xcode-select: error: tool 'xcodebuild' requires Xcode, but active developer directory '/Library/Developer/CommandLineTools' is a command line tools instance

命令行中可查看系统默认XCode安装路径:

$ xcode-select -p
/Library/Developer/CommandLineTools

正常的Xcode路径应该是:

/Applications/Xcode.app/Contents/Developer

解决办法:

使用如下命令切换Xcode为正确路径::

$ sudo xcode-select -s  +Xcode路径
(备注:Xcode路径可以将xcode直接拖入Terminal终端)

切换完之后,可以通过命令确认:

$ xcode-select -p

磁盘空间不足

Xcode运行会产生大量日志文件,导致Mac机器磁盘空间不足:

_images/qa_6_1.png

命令行,进入Xcode的缓存目录DerivedData:

$ cd ~/Library/Developer/Xcode/DerivedData/

可以看到该目录下有一个以“XCTestAgent”开头的目录,后缀是随机生成的字符串:

_images/qa_6_2.png

直接删除该目录下的子目录/Logs/Test下的全部文件:

_images/qa_6_3.png

由于信任文件问题导致的手机安装错误

使用qt4i的install接口安装App报错,日志堆栈如下:

_images/qa_7_1.png

这是因为iPhone手机信任Mac后在本地缓存的密钥对失效,所以要重新信任。进入缓存目录:

$ cd ~/.pymobiledevice/

清理目录下的所有文件:

_images/qa_7_2.png

重新插拔手机,会弹出信任弹框,选择“信任”:

_images/qa_7_3.png

如何阻止iOS设备的系统升级弹窗

iOS设备的系统升级弹窗频繁出现会影响自动化任务,如何在不升级系统的情况下关闭升级弹窗提醒,具体参考如下方法:

1、在手机上打开safari,地址栏输入:

https://beta.thuthuatios.com/tvos12/tvOS_12_Beta_Profile.mobileconfig

2、回车之后按照要求安装此provision文件即可,最后重启手机即可。

XCTestAgent编译报错

由于USB插口问题,导致连接通道请求被拒绝,可以通过更换USB插口解决该问题。

_images/qa_9.png

UISpy或者Eclipse安装打开报错

若当前Mac系统为10.12及以上版本,则首次有可能出现某些安装包已损坏、显示未激活、打开崩溃等的提示!原因是因为新系统屏蔽了任何来源的设置, 需要大家打开“允许任何来源”方可安装,具体步骤如下:

  • 打开命令行终端——Spotlight搜索(快捷键:command+空格或右上角搜索的符号):搜索“终端”

  • 输入命令,回车后输入你的Mac电脑密码:

    sudo spctl --master-disable
    
_images/spctl_1.png
  • 回到系统偏好设置的“安全与隐私”,勾选“允许任何来源”
_images/spctl_2.png
  • 如果以上步骤依旧不行,请将app移到 “/Applications” 目录。

接口文档

接口文档

qt4i.app Package

iOS App

class qt4i.app.App(device, bundle_id, trace_template=None, trace_output=None, **params)

基类:object

iOS App基类

add_rule_of_alert_auto_handle(message_text, button_text)

自动处理Alert规则,新增一项

参数:
  • message_text (str) – Alert内文本片段,支持正则表达式(范围是所谓文本,每个元素的label/name/value为文本段)
  • button_text (str) – Alert内按钮的文本,支持正则表达式(元素的label/name/value)
device

返回app所在的设备

返回类型:qt4i.device.Device
driver

返回app所使用的driver

返回类型:RPCClientProxy
flag_alert_auto_handled

自动关闭Alert框 :rtype: boolean

get_text(text)

获取text对应的本地语言文本

参数:text (str) – 标准文本,不随语言环境发生变化的唯一标识
返回:str - 本地语言的文
language

app的当前语言

返回类型:str
release()

终止APP

rules_of_alert_auto_handle

获取已设置的自动处理Alert规则

返回类型:list - [ {“message_text”: “message_text”, “button_text”: “button_text”}, .. ]
start()

启动APP

class qt4i.app.NLCType

基类:object

模拟弱网络类型

class qt4i.app.Preferences(device)

基类:qt4i.app.App

系统app 设置

reset_host_proxy()

关闭host代理

set_host_proxy(server, port, wifi_name)

设置host代理

参数:
  • server (str) – 服务器名
  • port (int) – 端口号
  • wifi (str) – wifi名
switch_network(network_type, nlc_type='None', timeout=1)

网络切换

参数:
  • network_type (int) – 网络类型,如下: 0:无WIFI无4G 1:无WIFi有4G 2:有WIFI无4G 3:有WIFI有4G 4:飞行模式 5:保持不变,仅设置弱网
  • nlc_type (NLCType) – 模拟弱网络类型
class qt4i.app.Safari(device=None, url_scheme=False)

基类:qt4i.app.App

Safari浏览器

find_by_url(url, page_cls=None, timeout=10)

在当前打开的页面中查找指定url,返回WebPage实例,如果未找到,返回None

参数:
  • url (str) – 要查找的页面url
  • page_cls (qt4w.webcontrols.WebPage) – 用户实现的WebPage子类,默认不填写则使用基类WebPage
  • timeout (int/float) – 查找超时时间,单位:秒
返回类型:

qt4w.webcontrols.WebPage

open_url(url, page_cls=None)

打开Safari浏览器,跳转指定网,返回page_cls类的实例

参数:
  • url (str) – url地址
  • page_cls (qt4w.webcontrols.WebPage) – 用户实现的WebPage子类,默认不填写则使用基类WebPage
返回类型:

qt4w.webcontrols.WebPage

qt4i.device Package

iOS设备模块

class qt4i.device.Device(attrs={}, devicemanager=None)

基类:object

iOS设备基类(包含基于设备的UI操作接口)

call_qt4i_stub(method, params, clazz=None)

QT4i Stub通用接口

参数:
  • method (str) – 函数名
  • params (list) – 函数参数
  • clazz (str) – 类名
返回:

插桩接口返回值

返回类型:

str

cleanup_log()

清理交互日志

click(x=0.5, y=0.5)

点击屏幕

参数:
  • x (float) – 横向坐标(从左向右,屏幕百分比)
  • y (float) – 纵向坐标(从上向下,屏幕百分比)
click2(element)

基于控件坐标点击屏幕(用于直接点击控件无效的场景,尽量少用)

参数:element (Element) – 控件对象
copy_to_local(bundle_id, remotepath, localpath=None, is_dir=False, is_delete=True)

拷贝手机中sandbox指定目录的文件到Mac本地

参数:
  • bundle_id (str) – app的bundle id
  • remotepath (str) – sandbox上的目录或者文件,例如:/Library/Caches/test/
  • localpath (str) – 本地的目录
  • is_dir (boolean) – remotepath是否为目录,默认为单个文件
返回:

拷贝到本地的文件列表

返回类型:

list or None

deactivate_app_for_duration(seconds=3)

将App置于后台一定时间

参数:seconds (int) – 秒,若为-1,则模拟按Home键的效果,也即app切到后台后必须点击app才能再次唤起
返回:boolean
double_click(x, y)

双击屏幕

参数:
  • x (float) – 横向坐标(从左向右计算,屏幕百分比)
  • y (float) – 纵向坐标(从上向下计算,屏幕百分比)
download_file(file_url, dst_path)

从网上下载指定文件到本地

参数:
  • file_url (str) – 文件的url路径,支持http和https路径, 需要对app插桩
  • dst_path (str) – 文件在手机上的存储路径,例如:/Documents
drag(from_x=0.9, from_y=0.5, to_x=0.1, to_y=0.5, duration=0.5)

回避屏幕边缘,全屏拖拽(默认在屏幕中央从右向左拖拽)

参数:
  • from_x (float) – 起点 x偏移百分比(从左至右为0.0至1.0)
  • from_y (float) – 起点 y偏移百分比(从上至下为0.0至1.0)
  • to_x (float) – 终点 x偏移百分比(从左至右为0.0至1.0)
  • to_y (float) – 终点 y偏移百分比(从上至下为0.0至1.0)
  • duration (float) – 持续时间(秒)
drag2(direct='Left')

回避屏幕边缘,全屏在屏幕中央拖拽

参数:direct (EnumDirect.Left|EnumDirect.Right|EnumDirect.Up|EnumDirect.Down) – 拖拽的方向
driver

设备所在的driver

返回类型:RPCClientProxy
flick(from_x=0.9, from_y=0.5, to_x=0.1, to_y=0.5)

回避屏幕边缘,全屏滑动/拂去(默认从右向左滑动/拂去) 该接口比drag的滑动速度快,如果滚动距离大,建议用此接口

参数:
  • from_x (float) – 起点 x偏移百分比(从左至右为0.0至1.0)
  • from_y (float) – 起点 y偏移百分比(从上至下为0.0至1.0)
  • to_x (float) – 终点 x偏移百分比(从左至右为0.0至1.0)
  • to_y (float) – 终点 y偏移百分比(从上至下为0.0至1.0)
flick2(direct='Left')

回避屏幕边缘,全屏在屏幕中央滑动/拂去

参数:direct (EnumDirect.Left|EnumDirect.Right|EnumDirect.Up|EnumDirect.Down) – 滑动/拂去的方向
flick3(from_x=0.5, from_y=0.8, to_x=0.5, to_y=0.2, repeat=1, interval=0.5, velocity=1000)

全屏连续滑动(默认从下向滑动) 该接口比flick2的滑动速度快,适用于性能测试

参数:
  • from_x (float) – 起点 x偏移百分比(从左至右为0.0至1.0)
  • from_y (float) – 起点 y偏移百分比(从上至下为0.0至1.0)
  • to_x (float) – 终点 x偏移百分比(从左至右为0.0至1.0)
  • to_y (float) – 终点 y偏移百分比(从上至下为0.0至1.0)
  • repeat (int) – 滑动的次数
  • interval (double) – 滑动的间隔时间(秒)
  • velocity (double) – 滑动的速度
get_app_list(app_type='user')

获取设备上的app列表

参数:app_type (str) – app的类型(user/system/all)
返回:list
返回类型:app列表,例如: [{‘com.tencent.demo’: ‘Demo’}]
get_crash_log(procname)

获取指定进程的最新的crash日志

参数:proc_name (str) – app的进程名,可通过xcode查看
返回:crash日志路径
返回类型:string or None
get_device_detail()

获取设备型号,颜色,内存大小等详细信息

get_driver_log(start_time=None)

获取driver日志 :param start_time 用例执行的开始时间 :type str

try-catch 说明:为了兼容client以及server端,对取失败用例日志的操作(后续接口更新可删除)

get_foreground_app_name()

获取前台app的名称

get_foreground_app_pid()

获取前台app的PID

get_icon_badge(app_name)

获取设备中app消息未读数

get_log(start_time=None)

获取交互日志 :param start_time 用例执行的开始时间 :type str

try-catch 说明:为了兼容client以及server端,对取失败用例日志的操作(后续接口更新可删除)

get_syslog(watchtime, process_name=None)

获取手机系统日志

install(app_path)

安装应用程序

参数:app_path (str) – ipa或app安装包的路径(注意:真机和模拟器的安装包互不兼容)
返回类型:boolean
ios_version

iOS版本

返回类型:str
keyboard

获取键盘

list_files(bundle_id, file_path)

列出手机上app中的文件或者目录

参数:
  • bundle_id (str) – app的bundle id
  • file_path (str) – sandbox上的目录或者文件,例如:/Library/Caches/test/
lock()

锁定设备(灭屏)

long_click(x, y, duration=3)

长按屏幕

参数:
  • x (float) – 横向坐标(从左向右,屏幕百分比)
  • y (float) – 纵向坐标(从上向下,屏幕百分比)
  • duration (float) – 持续时间(秒)
name

设备名

返回类型:str
print_uitree(need_back=False)

打印界面树

参数:need_back (boolean) – 是否需要返回UI Tree
返回:控件树
返回类型:dict or None
pull_file(bundle_id, remotepath, localpath=None, is_dir=False, is_delete=True)

拷贝手机中sandbox指定目录的文件到Mac本地

参数:
  • bundle_id (str) – app的bundle id
  • remotepath (str) – sandbox上的目录或者文件,例如:/Library/Caches/test/
  • localpath (str) – 本地的目录
  • is_dir (boolean) – remotepath是否为目录,默认为单个文件
返回:

拷贝到本地的文件列表

返回类型:

list or None

push_file(bundle_id, localpath, remotepath)

拷贝Mac本地文件到手机中sandbox的指定目录地

参数:
  • bundle_id (str) – app的bundle id
  • localpath (str) – 文件路径,支持本地文件路径和http路径
  • remotepath (str) – iPhone上的目录或者文件,例如:/Documents/
返回类型:

boolean

reboot()

重启手机

rect

屏幕大小

返回类型:Rectangle
release()

释放设备

remove_file(bundle_id, file_path)

删除手机上app中的文件或者目录(主要用于app的日志或者缓存的清理)

参数:
  • bundle_id (str) – app的bundle id
  • file_path (str) – sandbox上的目录或者文件,例如:/Library/Caches/test/
remove_files(bundle_id, file_path)

删除手机上app中的文件或者目录(主要用于app的日志或者缓存的清理)

参数:
  • bundle_id (str) – app的bundle id
  • file_path (str) – sandbox上的目录或者文件,例如:/Library/Caches/test/
reset_host_proxy()

关闭host代理

screenshot(image_path=None)

截屏,返回元组:截屏是否成功,图片保存路径

参数:image_path (str) – 截屏图片的存放路径
返回类型:tuple (boolean, str)
set_host_proxy(server, port, wifi)

设置host代理

参数:
  • server (str) – 服务器名
  • port (int) – 端口号
  • wifi (str) – wifi名
simulator

是否模拟器

返回类型:boolean
start_app(bundle_id, app_params, env, trace_template=None, trace_output=None, retry=5, timeout=55)

启动APP

参数:
  • bundle_id (str) – APP Bundle ID
  • app_params (dict) – app启动参数
  • env (dict) – app的环境变量
  • trace_template (str) – 专项使用trace_template路径,或已配置的项
  • trace_output (str) – 专项使用trace_output路径
  • retry (int) – 重试次数(建议大于等于2次)
  • timeout (int) – 单次启动超时(秒)
  • instruments_timeout (int) – 闲置超时(秒)
返回类型:

boolean

stop_app()

终止APP

返回类型:boolean
switch_network(network_type, nlc_type)

实现网络切换

参数:
  • network_type (int) – 网络类型,例如: 0:无WIFI无xG 1:无WIFi有xG 2:有WIFI无xG 3:有WIFI有xG 4:飞行模式
  • nlc_type (NLCType) – 模拟弱网络类型
udid

设备的udid

返回类型:str
uninstall(bundle_id)

卸载应用程序

参数:bundle_id (str) – APP的bundle_id,例如:com.tencent.qq.dailybuild.test
返回类型:boolean
unlock()

解锁设备

upload_photo(photo_path, album_name, cleared=True)

上传照片到系统相册

参数:
  • photo_path (str) – 本地照片路径
  • album_name (str) – 系统相册名
  • cleared (boolean) – 是否清空已有的同名相册
返回类型:

boolean

class qt4i.device.DeviceManager

基类:object

设备管理类,用于多个设备的申请、查询和管理(此类仅用于Device内部,测试用例中请勿直接使用)

static update_local_devices()

更新本地设备列表

class qt4i.device.DeviceResource(host, port, udid, is_simulator, name, version, csst_uri=None, resource_id=None)

基类:object

设备资源

class qt4i.device.DeviceServer(addr, port, udid=None, agent_port=8100, driver_type='xctest', endpoint_clss='')

基类:object

设备服务器

debug()

调试模式启动driverserver

exist()

driverserver进程是否存在

get_pid()

Returns the PID from pidfile

restart()

重启

start()

启动

stop()

停止

class qt4i.device.IOSDeviceResourceHandler(resource_lock_type=<class 'testbase.resource.LocalResourceLock'>)

基类:testbase.resource.LocalResourceHandler

iOS设备资源的本地管理

iter_resource(res_group=None, condition=None)

遍历全部资源(可以按照优先级顺序返回来影响申请资源的优先级)

参数:
  • res_type (str) – 资源类型
  • res_group (str) – 资源分组
  • condition (dict) – 资源属性匹配
返回:

iterator of resource, dict type with key ‘id’

Rtypes:

iterator(dict)

class qt4i.device.Keyboard(device)

基类:object

键盘

send_keys(keys)

键盘输入

参数:keys (str) – 要输入的字符串

qt4i.exception Package

iOS UI控件异常模块

exception qt4i.exceptions.ControlAmbiguousError

基类:exceptions.Exception

找到多个控件

exception qt4i.exceptions.ControlInvalidError

基类:exceptions.Exception

控件已失效

exception qt4i.exceptions.ControlNotFoundError

基类:exceptions.Exception

未找到控件

exception qt4i.exceptions.TimeoutError

基类:exceptions.Exception

超时异常

qt4i.icontrols Package

iOS基础UI控件模块

class qt4i.icontrols.ActionSheet(root, locator=None, **ext)

基类:qt4i.icontrols.Element

UIActionSheet弹出框

buttons

ActionSheet上面的所有按钮

click_button(text)

点击text对应的按钮

参数:text (str) – 按钮上的文本
class qt4i.icontrols.Alert(root, locator="/classname='UIAAlert' & maxdepth = 20", **ext)

基类:qt4i.icontrols.Element

弹出框

由于弹出框在iOS不同版本中的UI逻辑不一致(QPath不同),但业务逻辑相对统一(确定/取消),故单独封装成一个类

buttons

Alert控件上的所有按钮

click_button(text)

点击指定文本的按钮

参数:text (str) – 按钮上的文本
title

提示文本内容

class qt4i.icontrols.Button(root, locator, **ext)

基类:qt4i.icontrols.Element

Button控件

class qt4i.icontrols.Cell(element, locators={})

基类:object

TableCell控件

class qt4i.icontrols.ControlContainer

基类:object

控件集合接口

Controls

返回子控件集合 - 示例:XX_Window.Controls[‘XX_按钮’].click()

clearLocator()

清空控件定位参数

hasControlKey(control_key)

是否包含控件control_key

参数:control_key (str) – 控件名
返回类型:boolean
isChildCtrlExist(childctrlname, timeout={'interval': 0.005, 'timeout': 2.0})

判断子控件是否存在

参数:childctrlname (str) – 控件名
返回类型:boolean
updateLocator(locators)

更新控件定位参数

参数:locators (dict) – 定位参数,格式是 {‘控件名’:{‘type’:控件类, 控件类的参数dict列表}, …}
class qt4i.icontrols.Element(root, locator, **ext)

基类:qt4i.icontrols.ControlContainer

控件(UI框架是由各种控件组合而成)

children

获取子控件(仅含相对本控件的第二层子控件,不含第三层以及更深层的子控件,建议用此方法解决动态Path的场景)

返回:[Element, …]
返回类型:list
click(offset_x=None, offset_y=None)

点击控件

参数:
  • offset_x (float or None) – 相对于该控件的坐标offset_x,百分比( 0 -> 1 ),不传入则默认该控件的中央
  • offset_y (float or None) – 相对于该控件的坐标offset_y,百分比( 0 -> 1 ),不传入则默认该控件的中央
double_click(offset_x=0.5, offset_y=0.5)

双击控件

参数:
  • offset_x (float) – 相对于该控件的坐标offset_x,百分比( 0 -> 1 ),不传入则默认该控件的中央
  • offset_y (float) – 相对于该控件的坐标offset_y,百分比( 0 -> 1 ),不传入则默认该控件的中央
drag(from_x=0.9, from_y=0.5, to_x=0.1, to_y=0.5, duration=0.5)

回避控件边缘,在控件体内拖拽(默认在控件内从右向左拖拽)

参数:
  • from_x (float) – 起点 x偏移百分比(从左至右为0.0至1.0)
  • from_y (float) – 起点 y偏移百分比(从上至下为0.0至1.0)
  • to_x (float) – 终点 x偏移百分比(从左至右为0.0至1.0)
  • to_y (float) – 终点 y偏移百分比(从上至下为0.0至1.0)
  • duration (float) – 持续时间(秒)
drag2(direct='Left')

回避边缘在控件体内拖拽

参数:direct (EnumDirect.Left|EnumDirect.Right|EnumDirect.Up|EnumDirect.Down) – 拖拽的方向
enabled

控件是否开启

返回类型:boolean
exist()

控件是否存在

返回类型:boolean
find_elements(locator)

通过相对于当前父对象的Path搜索子孙控件(建议用此方法解决动态Path的场景)

参数:locator (QPath | str) – 相对当前对象的 - 子Path对象 或 子Path字符串 (搜索的起点为当前父对象)
返回:[Element, …]
返回类型:list
first_with_name(name)

通过name文本获取第一个匹配的子element

参数:name (str) – 子控件的name
返回类型:Element or None
first_with_predicate(predicate)

通过predicate文本获取第一个匹配的子element

参数:predicate (str) – 预期子element的predicate (例如:“name beginswith ‘xxx’”)
返回类型:Element or None
first_with_value_for_key(key, value)

通过匹配指定key的value,获取第一个匹配的子element

参数:
  • key (str) – key (例如:label、name、value)
  • value (str or int) – 对应key的value值
返回类型:

Element or None

flick(from_x=0.9, from_y=0.5, to_x=0.1, to_y=0.5)

滑动/拂去(默认从右向左回避边缘进行滑动/拂去)该接口比drag的滑动速度快,如果滚动距离大,建议用此接口

参数:
  • from_x (float) – 起点 x偏移百分比(从左至右为0.0至1.0)
  • from_y (float) – 起点 y偏移百分比(从上至下为0.0至1.0)
  • to_x (float) – 终点 x偏移百分比(从左至右为0.0至1.0)
  • to_y (float) – 终点 y偏移百分比(从上至下为0.0至1.0)
flick2(direct='Left')

回避边缘在控件体内滑动/拂去

参数:direct (EnumDirect.Left|EnumDirect.Right|EnumDirect.Up|EnumDirect.Down) – 滑动/拂去的方向
force_touch(pressure=1.0, duration=2.0)

3D touch

参数:
  • pressure (float) – 按压力度(取值:0-1.0)
  • duration (float) – 按压持续的时间(单位:秒)
get_attr_dict()

获取元素的属性信息,返回字典

返回类型:dict
get_metis_view()

返回MetisView

label

控件的label

返回类型:str or None
long_click(duration=3, offset_x=0.5, offset_y=0.5)

单指长按

参数:
  • duration (int) – 持续时间(秒)
  • offset_x (float) – 相对于该控件的坐标offset_x,百分比( 0 -> 1 ),不传入则默认该控件的中央
  • offset_y (float) – 相对于该控件的坐标offset_y,百分比( 0 -> 1 ),不传入则默认该控件的中央
name

控件的name

返回类型:str or None
parent

获取上一级的父控件

返回类型:Element
print_uitree()

打印当前控件的UI树

rect

控件的矩形信息

返回类型:Rectangle
screenshot(image_path=None, image_type=None)

截屏

Attention:

裁剪图像使用了PIL库,使用该接口请安装Pillow,如下:pip install Pillow

参数:
  • image_path (str) – 截屏图片的存放路径
  • image_path – 截屏图片的存放路径
返回:

截屏结果和截屏文件的路径

返回类型:

tuple (boolean, str)

scroll_to_visible(rate=1.0, drag_times=20)

自动滚动到元素可见(技巧: Path中不写visible=true,当对象在屏幕可视范围之外,例如底部,调用此方法可以自动滚动到该元素为可见)

参数:drag_times (int) – 最多尝试下滑次数,默认20次
send_keys(keys)

输入字符串

参数:keys (str) – 字符串内容
Attention:该接口不支持中文,中文输入请使用value=’中文’
value

控件的value

返回类型:None | str
visible

控件是否可见

返回类型:boolean
wait_for_exist(timeout, interval)

控件是否存在

参数:
  • timeout (float) – 超时值(秒),例如对象需要等待网络拉取10秒才出现
  • interval (float) – 轮询值(秒),轮询值建议 0.005 秒
返回类型:

boolean

with_name(name)

通过name文本获取匹配的子elements

参数:name (str) – 子控件的name
返回类型:list
with_predicate(predicate)

通过predicate文本获取第一个匹配的子element :param predicate: 预期子element的predicate (例如:“name beginswith ‘xxx’”) :type predicate: str :rtype: Element or None

with_value_for_key(key, value)

通过匹配指定key的value,获取所有匹配的子element

参数:
  • key (str) – key (例如:label、name、value)
  • value (str or int) – 对应key的value值
返回类型:

Element or None

class qt4i.icontrols.MetisView(element)

基类:object

click(offset_x=None, offset_y=None)

点击

参数:
  • offset_x (float or None) – 相对于该控件的坐标offset_x,百分比( 0 -> 1 ),不传入则默认该控件的中央
  • offset_y (float or None) – 相对于该控件的坐标offset_y,百分比( 0 -> 1 ),不传入则默认该控件的中央
drag(from_x=0.5, from_y=0.5, to_x=0.5, to_y=0.1, duration=0.5)

拖拽

参数:
  • from_x (float) – 起点 x偏移百分比(从左至右为0.0至1.0)
  • from_y (float) – 起点 y偏移百分比(从上至下为0.0至1.0)
  • to_x (float) – 终点 x偏移百分比(从左至右为0.0至1.0)
  • to_y (float) – 终点 y偏移百分比(从上至下为0.0至1.0)
  • duration (float) – 持续时间(秒)
os_type

系统类型,例如”android”,”ios”,”pc”

rect

元素相对坐标(x, y, w, h)

screenshot()

当前容器的区域截图

class qt4i.icontrols.PickerWheel(root, locator, **ext)

基类:qt4i.icontrols.Element

滚轮选择框

class qt4i.icontrols.SecureTextField(root, locator, **ext)

基类:qt4i.icontrols.Element

密码输入框

class qt4i.icontrols.Slider(root, locator="/classname='UIASlider' & maxdepth = 20", **ext)

基类:qt4i.icontrols.Element

进度条

value

控件的value

返回类型:None | str
class qt4i.icontrols.TableView(root, locator=None, **ext)

基类:qt4i.icontrols.Element

TableView控件

class qt4i.icontrols.TextField(root, locator, **ext)

基类:qt4i.icontrols.Element

文本输入框

class qt4i.icontrols.Window(root, locator=None, **ext)

基类:qt4i.icontrols.Element

窗口基类

qt4i.itestcase Package

qt4i测试用例基类模块

class qt4i.itestcase.iTestCase(testdata=None, testdataname=None, attrs=None)

基类:testbase.testcase.TestCase

QT4i测试用例基类

cleanTest()

测试用例清理,以用例为单位清理日志,并释放所有设备

clean_test()

测试用例清理,以用例为单位清理日志,并释放所有设备

get_crash_log(procname)

获取应用程序的crash日志

参数:procname (str) – app的进程名,可通过xcode查看
返回:string or None - crash日志路径
get_extra_fail_record()

当错误发生时,获取需要额外添加的日志记录和附件信息

返回:dict,dict - 日志记录,附件信息
initTest(testresult)

测试用例初始化

init_test(testresult)

测试用例初始化

qt4i.qpath Package

QPath

class qt4i.qpath.QPath(qpath=None)

基类:str

QPath基类

qt4i.util Package

utility module

class qt4i.util.EnumDirect

基类:object

方向

class qt4i.util.Rectangle(left, top, right, bottom)

基类:object

UI控件的矩形区域

class qt4i.util.RegExpCompile(source)

基类:object

编译正则表达式 当字符串变量中出现正则中的逻辑运算符时,该类将会自动处理转义。

class qt4i.util.Timeout(timeout=1.8, interval=0.005)

基类:testbase.util.Timeout

超时

qt4i.util.less_to(version_a, version_b)

判断version_a是否小于version_b

返回:True if version_a less than version_b, False if not
返回类型:boolean

qt4i.web Package

iOS WebView

class qt4i.web.IOSWebDriver(webview)

基类:qt4w.webdriver.webkitwebdriver.WebkitWebDriver

iOS WebKit WebDrvier

class qt4i.web.IOSWebView(root, locator, title=None, url=None, **ext)

基类:qt4i.icontrols.Element

iOS 内嵌WebView控件

click(x_offset, y_offset)

点击控件

参数:
  • offset_x (float or None) – 相对于该控件的坐标offset_x,百分比( 0 -> 1 ),不传入则默认该控件的中央
  • offset_y (float or None) – 相对于该控件的坐标offset_y,百分比( 0 -> 1 ),不传入则默认该控件的中央
double_click(x_offset, y_offset)

双击控件

参数:
  • offset_x (float) – 相对于该控件的坐标offset_x,百分比( 0 -> 1 ),不传入则默认该控件的中央
  • offset_y (float) – 相对于该控件的坐标offset_y,百分比( 0 -> 1 ),不传入则默认该控件的中央
drag(x1, y1, x2, y2)

回避控件边缘,在控件体内拖拽(默认在控件内从右向左拖拽)

参数:
  • from_x (float) – 起点 x偏移百分比(从左至右为0.0至1.0)
  • from_y (float) – 起点 y偏移百分比(从上至下为0.0至1.0)
  • to_x (float) – 终点 x偏移百分比(从左至右为0.0至1.0)
  • to_y (float) – 终点 y偏移百分比(从上至下为0.0至1.0)
  • duration (float) – 持续时间(秒)
eval_script(frame_xpaths, script)

javascript脚本注入接口

参数:
  • frame_xpaths (str|None) – 保留,暂不使用
  • script (str) – javascript脚本
long_click(x_offset, y_offset, duration=2)

单指长按

参数:
  • duration (int) – 持续时间(秒)
  • offset_x (float) – 相对于该控件的坐标offset_x,百分比( 0 -> 1 ),不传入则默认该控件的中央
  • offset_y (float) – 相对于该控件的坐标offset_y,百分比( 0 -> 1 ),不传入则默认该控件的中央
rect

WebView控件的坐标

send_keys(keys)

输入字符串

参数:keys (str) – 字符串内容
Attention:该接口不支持中文,中文输入请使用value=’中文’
visible_rect

WebView控件可见区域的坐标信息

webdriver_class

WebView对应的WebDriver类

class qt4i.web.QT4iBrowserWin(app)

基类:qt4i.icontrols.Window

浏览器窗口基类

webview

WebView对象