QTA UI自动化基础文档

内容:

快速入门

在编写UI自动化测试之前,您需要先准备好以下的工具和开发环境:

  • Python运行环境
  • QTAF(testbase & tuia)
  • 一个适用于对应平台QTA测试驱动器,如QT4A

在开始之前,您需要先创建一个QTA测试自动化项目,并掌握基本的测试用例组织形式和设计方法,详情请参考《QTA Testbase文档》。

注解

为了简单,下文示例代码使用QTA的一个驱动器————QT4X,QT4X是一个示例的QTA测试驱动器,支持对“QT4X SUT UI框架”实现的应用的自动化测试。“QT4X SUT UI框架”其实并不是一个真正的UI应用框架,而只是用于QTA驱动器的测试和学习目的设计,基于Python的XML etree实现的“伪”框架。实际使用时,可以针对测试的平台选择对应的驱动器进行替换。

准备一个测试项目

可以使用QTAF包创建一个空测试项目,比如:

$ qta-manage createproject foo

创建成功后,安装对应平台的“测试驱动器”:

$ pip install https://github.com/qtacore/QT4x/archive/master.zip

封装一个App

对于QTA,一个App表示一个被测的应用。我们在测试项目的foolib目录下创建一个新的Python模块“fooapp.py”:

from qt4x.app import App

class FooApp(App):

   ENTRY = "qt4x_sut.demo"
   PORT  = 16000

注解

由于平台的差异,各个QTA的驱动器封装App的方式略不同,具体需要参考对应驱动器的文档。对于QT4X,这里“ENTRY”指定的是一个Python的模块名,标识被测应用代码的入口;而“PORT”指定的是测试桩RPC服务的监听端口。

封装一个Window

对于QTA,一个Window表示被测应用的一个用户界面。一般来说,QTA的Window的概念和被测平台都能找到对应的概念,比如Android的Activity、iOS的Window、Win32平台上的窗口。 QTA的窗口的封装是为了告知QTA框架以下的信息:

  • 如果找到一个窗口
  • 窗口内的UI元素的布局信息

因此,在封装窗口的时候,我们需要获取被测应用界面的窗口和对应的布局信息,这就需要利用对应平台下的UI布局查看工具:

平台 驱动器 UI查看工具
Android QT4A AndroidUISpy
iOS QT4i iOSUISpy
Win32 QT4C QTAInspect
Web QT4W 可以在具体平台上的工具内部调起相关工具

对于QT4X,我们的UI窗口是由一个XML定义的,具体可以查看“qt4x_sut.demo”模块定义的窗口布局XML,比如主窗口布局如下:

MAIN_LAYOUT = """<Window>
    <TitleBar name="title" text="Text editor"/>
    <MenuBar name="menu" >
        <Menu text="File" >
            <MenuItem value="Open file" />
            <MenuItem value="Save file" />
        </Menu>
        <Menu text="Help">
            <MenuItem value="Help" />
            <MenuItem value="About">
                <Buttom name="about_btn"/>
            </MenuItem>
        </Menu>
    </MenuBar>
    <TextEdit name="editor" text="input here..." />
</Window>"""

被测应用在创建时注册了主窗口:

class TextEditorApp(App):
    '''text editor app
    '''
    def on_created(self):
        self.register_window("Main", MAIN_LAYOUT)
         ...

可以看到注册的窗口标识符为“Main”,因此我们封装对应的窗口:

from qt4x.app import Window

class MainWindow(Window):
    """main window
    """
    NAME = "Main"

注解

由于平台的差异,各个QTA的驱动器封装Window的方式略不同,甚至一些平台下的类名字都不同,具体需要参考对应驱动器的文档。对于QT4X SUT的UI框架,一个窗口可以通过其注册时的标识符唯一确定,所以QT4X要求窗口封装的时候需要指定这个“标识符”,也就是“NAME”属性

指定窗口布局

以上的窗口的封装只是告诉测试框架如何找到这个窗口,还需要指定这个窗口的布局。所谓窗口的布局,即是一个窗口中的UI元素的信息,包括:

  • 名称————便于后面使用这个UI元素
  • UI元素类型————指定这个UI元素可以被如何使用
  • UI元素的定位器————如何定位此UI元素

QTA框架通过在窗口类的updateLocator接口来设置窗口的布局信息,比如对应上面的窗口,我们封装一个UI元素的布局:

from qt4x.controls import Button, Window
from qt4x.qpath import QPath

class MainWindow(Window):
    """main window
    """
    NAME = "Main"

    def __init__(self, app):
        super(MainWindow, self).__init__(app)
        self.update_locator({
            "About Button": {"type": Button, "root": self, "locator": QPath("/name='menu' /name='about_btn' && MaxDepth=4 ")}
        })

这里是重载了窗口的构造函数,并追加调用updateLocator接口来设置窗口布局。

注解

虽然在各个平台下的都是通过updateLocator来设置窗口布局,但是由于各个平台下的Window类的构造函数的参数可能不同,重装构造函数时需要注意。

从updateLocator的调用参数看,窗口布局是一个Python的dict结构:

  • 字典的键就是UI元素的“名称”,主要用于后面使用这个UI元素时所谓索引,一般这个名称建议是一个可读性较好且和被测应用的功能业务相关的名字;

  • 字典的值也是一个Python的字典,标识UI元素的属性,字典包括一下内容:

    • type: 标识UI元素元素的类型,对应一个Python类
    • locator:UI元素元素的定位器
    • root:UI元素元素定位查找时的使用的根节点UI元素

注解

UI元素属性的type和root的属性有可能不是必填属性,具体请参考对应平台的驱动器的接口

在上面的简单的例子中,我们定义了一个“About Button”的按钮(Button)UI元素。这里存在两个问题,一是,在实际的UI布局的封装的时候,该如何为一个UI元素指定一个准确的UI元素类型呢? QTA框架对UI元素的类型并不做任何校验和检查的,也就是说,如果指定了一个UI元素类型为Button,则这个UI元素就可以当作一个Button来使用,即使这个UI元素实际上并不是一个Button,而是一个InputUI元素也是可以在QTA框架中当作Button来使用(当然,最终执行的结果会导致异常)。 因此,选择一个UI元素的类型就是看需要对这个UI元素进行哪些操作,一般来说,QTA各个平台下的基础UI元素类型在名字上都会尽量和对应的UI框架的UI元素类名一致,但也不排除有例外的情况。在选择UI元素的类型时,可以查看QTA各个平台的驱动器的UI元素类定义对应的Python模块:

平台 驱动器 Python模块
Android QT4A qt4a.andrcontrols
iOS QT4i qt4i.icontrols
Win32 QT4C tuia.wincontrols
Web QT4W qt4w.webcontrols
Demo QT4X qt4x.controls

UI元素定位的第二个问题就是如何设计“UI元素的定位器”。由于在目前已知的所有的UI框架中,UI元素的组织结构都是树状的,包括我们这里使用的QT4X使用的XML结构也是树状的。 因此,QTA的UI元素的定位的本质就是在树状的结构中准确找到指定的节点,为此,QTA框架需要有两个参数:

  • 起始查找节点,也就是前面U元素属性的“root”参数就是用于指定UI树查找的根节点
  • UI元素定位器

UI树查找的根节点很容易理解,来看看UI元素的定位器。QTA框架使用了多种类型的UI元素的定位器:

  • QPath:QTA最主要的UI元素定位
  • XPath:主要用于Web UI元素的定位,XPath有W3C定义(https://www.w3.org/TR/xpath/
  • str:各个平台下意义不同,一般是QPath或XPath的一种简便形式

由于XPath W3C是标准定义的语言,这里就不再熬述。 而QPath是QTA框架定义的一种简单的UI定位语言,以上面的QPath的例子:

QPath("/name='menu' /name='about_btn' && MaxDepth=4 ")

以上的QPath查找控件的过程如下:

  1. 从根节点(窗口容器)开始查找其直接孩子节点,如果节点对应的UI元素的属性存在name属性且取值为menu,则执行第二级查找
  2. 开始第二级查找,以第一次查找的结果对象的UI元素为根,查找深度小于或等于4的全部孩子节点对应的UI元素,如果其属性存在name属性且取值为about_btn,则就是要定位的目标UI元素

以上只是简单的例子,更多关于QPath的语法和使用方法说明,可以参考《./uilocator》。

QPath语法和使用

QPath是QTA UI自动化框架中最主要的定位UI元素的语言。

QPath的语法

QPath的完整的语法定义如下:

QPath ::= Seperator QPath Seperator UIObjectLocator
 UIObjectLocator ::= UIObjectProperty PropertyConnector UIObjectLocator
 UIObjectProperty ::= UIProperty
                     | RelationProperty
                     | IndexProperty
                     | UITypeProperty
 UIProperty ::= PropertyName Operator Literal
 RelationProperty ::= MaxDepthIdentifier EqualOperator Literal
 IndexProperty ::= InstanceIdentifier EqualOperator Literal
 UITypeProperty ::= UITypeIdentifier EqualOperator StringLiteral
 MaxDepthIdentifier := "MaxDepth"
 InstanceIdentifier ::= "Instance"
 UITypeIdentifier := "UIType"
 Operator ::= EqualOperator | MatchOperator
 PropertyConnector ::= "&&" | "&"
 Seperator ::= "/"
 EqualOperator ::= "="  #精确匹配
 MatchOperator ::= "~="  #使用正则进行模糊匹配
 PropertyName  ::= "[a-zA-Z_]*"
 Literal := StringLiteral
           | IntegerLiteral
           | BooleanLiteral
 StringLiteral ::= "\"[a-zA-Z_]*\""
 IntegerLiteral ::= "[0-9]*"
 BooleanLiteral := "True" | "False" | "true" | "false"

需要注意的是:

  • QPath的属性名(PropertyName)都是不区分大小写

以下是一个最简单的QPath的例子:

/ Id='MainPanel'

QPath通过“/”来连接多个UI对象定位器(UIObjectLocator),比如:

/ Id='MainPanel' / Id='LogoutButton'

一个UI对象定位器也可以存在多个属性,使用“&”或者“&&”连接,比如:

/ Id='MainPanel' & className='Window' / Id='LogoutButton' && Visiable='True'

如果属性值本身是布尔或者数值类型,也可以不使用字符串标识,比如下面的QPath和之前的QPath是等价的:

/ Id='MainPanel' & className='Window' / Id='LogoutButton' && Visiable=True

QPath也支持对属性进行正则匹配,可以使用“~=”:

/ Id~='MainP.*' / Id='LogoutButton'

QPath和UI元素查找

QPath的语义是描述如何去定位一个或者多个UI元素。因为在所有的UI框架中,UI元素的组织形式都是树状的, 因此QPath描述的也就是如何在一个UI树里面去定位一个或者多个UI元素。 QPath在树查找的过程,就是从根节点开始,一层一层增加树的深度去查找UI元素,QPath的间隔符“/”就是为了区分层次,比如下面的QPath:

/ Id='MainPanel' & className='Window' / Id='LogoutButton' && Visiable='True'

有两层的查找。 在每一层的查找过程中,QPath通过属性匹配的方式来筛选UI元素。在一些情况下,如果UI元素不存在QPath中定义的属性,则认为不匹配。 QPath在一层深度中查找到一个或者多个UI元素,则这些UI元素都会作为根,依次查找下一层的UI元素;如果在一层深度的UI元素查找中未能找到任何匹配的UI元素,则查找失败,即这个QPath不匹配任何的UI元素。

QPath的特殊属性

一般来说,QPath的查找都是匹配UI元素的属性,但QPath也定义了一些特殊的属性,

MaxDepth属性

MaxDepth属性用于控制一次搜索的最大深度。一般情况下,一个QPath有多少个UIObjectLocator则有会匹配到同样深度的子UI元素,但是对于UI元素层次比较深的UI元素,QPath会变得很长。 比如下面的例子:

/ ClassName='TxGuiFoundation' && Caption~='QQ\d+'
/ ClassName='TxGuiFoundation'
/ ClassName='TxGuiFoundation'
/ ClassName='TxGuiFoundation'
/ ClassName='TxGuiFoundation'
/ ClassName='TxGuiFoundation'
/ ClassName='TxGuiFoundation'
/ ClassName='GF' && name='mainpanel'

为解决这个问题,QPath使用关系属性(RelationProperty)来指定一次搜索的深度,比如上面很冗余的QPath在很多情况下都可以修改为:

/ ClassName='TxGuiFoundation' && Caption~='QQ\d+'
/ name='mainpanel'&& MaxDepth='7'

注意MaxDepth是从当前深度算起,也就是说MaxDepth='1'时和没有指定MaxDepth是一样的,也就是说MaxDepth默认为1。

Instance属性

当一个QPath搜索的结果中包含多个匹配的UI元素时,可以使用索引属性(IndexProperty)来唯一指定一个UI元素,比如假设以上的QPath搜索得到了多个UI元素,需要指定返回第一个匹配的UI元素,则可以写为:

/ ClassName='TxGuiFoundation' && Caption~='QQ\d+'
/ name='mainpanel'&&MaxDepth='7'&&Instance='0'

注意索引是从0开始计算的,支持负数索引。

注解

当QPath找到多个UI元素的时候其排序本身并没有标准的定义,一般来说和树遍历的方式有关,具体的平台可能都存在差异,而且由于指定Instance本身并不是很好维护的方式,所以要尽量避免使用。

UIType属性

当UI界面混合使用多种类型的UI控件时,可以通过UIType指定控件的类型,如果不指定UIType,则继承上一层搜索时指定的UIType,对于只有一种控件类型的平台,可以指定一个默认UIType,用户可以不用在QPath显式指定UIType。比如以下一个使用UIType的例子:

/ ClassName='TxGuiFoundation' && Caption~='QQ\d+'
/ UIType='GF'&&name='main'
/ name='mainpanel'

在这里,UIType默认为“Win32”,因此在第一层搜索的搜索时候,只搜索控件类型为Win32的控件;但在第二层搜索的时候,UIType指定为“GF”,则第二层和第三层的搜索都只搜索控件类型为GF的控件。

QPath简便写法汇总

在一些平台上,可以使用字符串类型来标识一个等价意义的QPath,特汇总如下。

QT4C的字符串类型定位器

在QT4C中字符串类型的定位器等价于在UI元素树查找对应Name属性值匹配的元素,比如字符串:

"Hello"

等价于下面的“伪”QPath:

QPath('/Name="Hello"' && MaxDepth=∞')

QT4i的字符串类型定位器

在QT4i中字符串类型的定位器等价于在UI元素树查找对应Name属性值匹配的元素,比如字符串:

"Hello"

等价于下面的“伪”QPath:

QPath('/Name="Hello"' && MaxDepth=∞')

QPath兼容性问题汇总

由于历史原因,QTA的各个UI自动化的驱动器在QPath和UI元素查找的实现上略有差异,特汇总如下。

QT4A的首级Id无限深度查找

对于QT4A的QPath,如果QPath的第一层的Selector只有一个Id的属性,则在搜索UI元素的时候遍历的深度为无限。比如下面的QPath:

QPath('/Id="Hello"')

在QT4A中等价于下面的“伪”QPath:

QPath('/Id="Hello"' && MaxDepth=∞')

因为不存在值为“无限”的数值,所以上面的QPath其实是不合法的。

QT4A的Instance属性

对于QT4A的QPath,如果Instance的取值类型为字符串,则Instance的值表示的是第N个元素(从1开始计算); 如果Instance的取值类型为数值,则Instance的值表示的是第N-1元素(从0开始计算)。 比如下面的QPath:

QPath('/Id="Hello" && Instance="1" ')

和下面的QPath是等价的:

QPath('/Id="Hello" && Instance=0 ')

一般来说,在QT4A中推荐使用第二种用法;第二种用法和其他平台的驱动器的与语义是一致的。

接口文档

Submodules

tuia.qpathparser module

QPath解析器

QPath是一个用于定位各个平台的UI控件(除Web控件)的查询语言。

  • QPath语法定义如下:

    QPath ::= Seperator QPath Seperator UIObjectLocator
    UIObjectLocator ::= UIObjectProperty PropertyConnector UIObjectLocator
    UIObjectProperty ::= UIProperty 
                        | RelationProperty 
                        | IndexProperty
                        | UITypeProperty 
    UIProperty ::= PropertyName Operator Literal
    RelationProperty ::= MaxDepthIdentifier EqualOperator Literal
    IndexProperty ::= InstanceIdentifier EqualOperator Literal
    UITypeProperty ::= UITypeIdentifier EqualOperator StringLiteral
    MaxDepthIdentifier := "MaxDepth"
    InstanceIdentifier ::= "Instance"
    UITypeIdentifier := "UIType"
    Operator ::= EqualOperator | MatchOperator
    PropertyConnector ::= "&&"
    Seperator ::= "/"
    EqualOperator ::= "="  #精确匹配
    MatchOperator ::= "~="  #使用正则进行模糊匹配
    PropertyName  ::= "[a-zA-Z_]*"
    Literal := StringLiteral
              | IntegerLiteral
              | BooleanLiteral
    StringLiteral ::= ""[a-zA-Z_]*""
    IntegerLiteral ::= "[0-9]*"
    BooleanLiteral := "True" | "False" | "true" | "false"
    
  • 需要注意的是,QPath的属性名都是大小写无关的。

  • 简单举例如下:

    / ClassName='TxGuiFoundation' && Caption~='QQ\d+' / name='mainpanel'
    
class tuia.qpathparser.Literal(value, lexpos)

基类:object

QPath属性常量值

class tuia.qpathparser.Operator(value, lexpos)

基类:object

QPath操作符

class tuia.qpathparser.PropertyName(value, lexpos)

基类:object

QPath属性名

class tuia.qpathparser.QPathLexer

基类:object

QPath词法解析器

bad_match = '~[^=*]'
bad_octal_constant = '0[0-7]*[89]'
boolean_constant = '(True)|(False)|(true)|(false)'
decimal_constant = '(0)|([1-9][0-9]*)'
escape_sequence_1 = '(?<=\\\\)"'
escape_sequence_2 = "(?<=\\\\)'"
hex_constant = '0[xX][0-9a-fA-F]+'
hex_digits = '[0-9a-fA-F]+'
hex_prefix = '0[xX]'
input(qpath_string)

词法分析输入

参数:qpath_string (str) -- QPath字符串
octal_constant = '0[0-7]+'
string_char_1 = '([^"]|(?<=\\\\)")'
string_char_2 = "([^']|(?<=\\\\)')"
string_literal = '("([^"]|(?<=\\\\)")*")|(\'([^\']|(?<=\\\\)\')*\')'
string_literal_1 = '"([^"]|(?<=\\\\)")*"'
string_literal_2 = "'([^']|(?<=\\\\)')*'"
t_AND = '(&&)|(&)'
t_BAD_MATCH(t)
t_BOOL_CONST(t)
t_EQUAL = '='
t_INT_CONST_DEC(t)
t_INT_CONST_HEX(t)
t_INT_CONST_OCT(t)
t_MATCH = '~='
t_MINUS = '-'
t_PROPERTY = '[a-zA-Z_][0-9a-zA-Z_]*'
t_SEPERATOR = '/'
t_STRING_LITERAL(t)
t_error(t)
t_ignore = '\t '
token()

解析并返回一个Token

tokens = ['SEPERATOR', 'EQUAL', 'MATCH', 'AND', 'BOOL_CONST', 'STRING_LITERAL', 'INT_CONST_DEC', 'INT_CONST_OCT', 'INT_CONST_HEX', 'PROPERTY', 'MINUS']
class tuia.qpathparser.QPathParser(verbose=False)

基类:object

QPath语法解析器

QPath解析器解析QPath后会生成两个结构:

1. QPath结构列表:每一个UIObjectLocator用一个字典表示 其中字典的键值为属性名,对应的值为一个长度为2的列表,第一个元素为操作符号, 目前为字符串'='或'~=',第二个元素为属性值

2. QPath词法位置信息:和解析后的结构对应,每一个UIObjectLocator用一个字典表示 其中字典的键值为属性名,对应的指为一个长度为3的列表,第一个元素为属性名的 词法位置,第二个元素为操作符的词法位置,第三个元素为属性值的词法位置

比如以下的QPath:

/ ClassName="TxGuiFoundation" && Caption1~='QQ\d+' && Instance=-1 / UIType='GF' && name='mainpanel' && MaxDepth=10

解析后得到的结构列表为:

[{'Caption1': ['~=', 'QQ\d+'],
  'ClassName': ['=', 'TxGuiFoundation'],
  'Instance': ['=', -1]},
 {'MaxDepth': ['=', 10], 
  'UIType': ['=', 'GF'], 
  'name': ['=', 'mainpanel']}]

解析后得到的词法位置信息表为:

[{'CAPTION1': [33, 41, 43],
  'CLASSNAME': [2, 11, 12],
  'INSTANCE': [54, 62, 63]},
 {'MAXDEPTH': [103, 111, 112], 
  'NAME': [83, 87, 88], 
  'UITYPE': [68, 74, 75]}]

使用方法示例:

qp ="""/ ClassName="TxGuiFoundation" && Caption1~='QQ\d+' && Instance='-1' / UIType='GF' && name='mainpanel' && MaxDepth='10'"""
parser = QPathParser()
qpath_struct, lex_info = parser.parse(qp)
INT_TYPE_PROPNAMES = ['INSTANCE', 'MAXDEPTH']
p_error(p)

处理错误

p_int_const(p)

int_const : INT_CONST_DEC | INT_CONST_OCT | INT_CONST_HEX | MINUS int_const

p_object_locator(p)

object_locator : prop | object_locator AND prop

p_operator(p)

operator : EQUAL | MATCH

p_prop(p)

prop : prop_name operator prop_value

p_prop_name(p)

prop_name : PROPERTY

p_prop_value(p)

prop_value : STRING_LITERAL | BOOL_CONST | int_const

p_qpath(p)

qpath : SEPERATOR qpath_content

p_qpath_content(p)

qpath_content : | object_locator | qpath_content SEPERATOR object_locator

parse(qpath_string)

返回解析后的结果

参数:qpath_string (string) -- QPath字符串
返回:list, list - 解析后结构, 词法位置信息
exception tuia.qpathparser.QPathSyntaxError(qpath_string, err_msg, lexpos)

基类:exceptions.Exception

QPath语法错误

class tuia.qpathparser.UIObjectLocator(properties)

基类:object

QPath Locator

append(prop)

增加一个属性

参数:prop (UIObjectProperty) -- 属性
dumps()

序列化

返回:list
format()

格式化字符串

返回:str
class tuia.qpathparser.UIObjectProperty(name, operator, value)

基类:object

QPath属性

format()

格式化字符串

返回:str

tuia.qpathparser module

异常模块定义

exception tuia.exceptions.ControlAmbiguousError

基类:exceptions.Exception

找到多个控件

exception tuia.exceptions.ControlExpiredError

基类:exceptions.Exception

控件失效错误

exception tuia.exceptions.ControlNotFoundError

基类:exceptions.Exception

控件没有找到

exception tuia.exceptions.TimeoutError

基类:exceptions.Exception

超时异常

Module contents

Tuia:Tencent UI Automation Framework