在进行 Android App 持续集成性能测试的时候,需要自动化实现 UI 层面的一些操作,常见的几种场景包括:

这些场景虽然看上去互不相关,但是从测试的角度,UI 层面的操作应该都可以归为两类:控件定位和执行动作。

本文将从测试的角度出发,介绍 Android UI 实现自动化测试的基本方法,并着重讲解通过 Python 操作 Android UI 的一般性流程。后续,我会在单独的博客文章中介绍 UI 操作在 Android App 持续集成性能测试中的应用。

先说 uiautomator

要对 Android 的 UI 实现自动化操作,首先想到的就是 Google 官方的UI Automator,通过这个工具,可以很好地实现 Android UI 自动化。

UI Automator是一个从 Android 4.3 (API level 18) 引入的测试框架,它提供了一套丰富的 API,可以在不依赖于目标 app 内部实现机制的基础上,方便地创建自动化测试用例,实现用户对 Android UI 各种界面交互操作的模拟。

对于UI Automator的使用介绍,我从创建测试用例和执行测试两部分进行。

首先是创建测试用例,流程大致如下:

创建好了测试用例,那要怎样执行呢?

从 Android 4.3 开始,系统中就自带了uiautomator命令,命令的路径为/system/bin/uiautomator

由于uiautomator命令是运行在 Android 设备中的,因此需先要将编译好的 jar 文件 push 到 Android 设备中,导入目录为/data/local/tmp/

➜  adb push memorytest.jar /data/local/tmp/

完成以上准备工作后,就可以在 Android 的 Terminal 中执行了uiautomator命令了。

详细的uiautomator命令用法可参考官方文档,这里只列出最常用的一种方式:

➜  adb shell
shell@hammerhead:/ $ uiautomator runtest memorytest.jar -c com.uc.util.TestCases#slideScreen -e pkgName com.UCMobile

在如上示例中,memorytest.jar是我们之前编译好的测试用例 jar 文件名,com.uc.util.TestCases#slideScreen是 Java 工程中的类名和方法名,-e后面是传入测试类的name-value参数。

这里就不再对UI Automator进行过多介绍,后续我会再针对UI Automator单独写一篇更加详细的教程。

Python 调用 uiautomator

通常,我们的持续集成性能测试代码是采用 Python 编写的,那如何通过 Python 调用 uiautomator 呢?

如果沿用上面介绍的流程,Python 调用 uiautomator 实现自动化测试应该也会采用同样的思路:

首先,需要在 Java Project 使用UI Automator API编写 UI 测试场景,编译生成 jar 文件,并将这个文件导入到 Python 项目中。

然后,在 Python 测试代码中,调用uiautomator命令前需要先将 jar 文件 push 到 Android 设备。

jar_file_path = os.path.join(_project_root_path, "resource/jar/memorytest.jar")
cmdexec.push(jar_file_path, '/data/local/tmp/')

接下来,就可以在 Python 中组装测试命令,并将命令传到 Android 设备中进行执行。

cmd = "uiautomator runtest memorytest.jar -c com.uc.util.TestCases#slideScreen -e pkgName com.UCMobile"
cmdexec.sendShellCommand(cmd, timeout_time=None)

需要说明的是,上面代码中的cmdexec是一个封装类的实例,主要实现的是通过 adb 与 Android 设备进行交互,例如 push/pull 文件、执行 Android shell 命令等。

经过这么一个流程,可能大家都会感觉到实现起来太过复杂。特别地,如果需要增加一个测试场景,就又要到 Java 项目中添加测试代码,重新编译为 jar 文件,并将新的 jar 文件添加到 Python 项目中,或者替换原有 jar 文件。这还不算完,同样地,在 Python 项目中也需要针对新增的测试场景进行相应的编码。

难道就没有更便捷的方式么?

幸运的是,之前已经有人针对这个痛点填了坑,并在 GitHub 上进行了开源,项目名称是xiaocong/uiautomator(为了便于与 Google 官方的 uiautomator 进行区分,后面统一采用 pyuiautomator 进行描述)。它实现的功能很明确,从项目简介就一目了然。

Python wrapper of Android uiautomator test tool.

该工具以Python package的形式存在,可通过pip在测试机(PC)上进行安装。

pip install uiautomator

安装完毕后,无需在 Android 设备上安装任何东西,只要设备通过 adb 与主机相连,就可以在主机中通过 Python 操作 Android 设备的 UI 控件。

如下是简单的示例:

from uiautomator import device as d

# Turn on screen
d.screen.on()

# press back key
d.press.back()

# click (x, y) on screen
d.click(x, y)

# check unchecked checkbox
checkbox = d(className='android.widget.CheckBox', checked='false')
checkbox.click()

# click button with text 'Next'
d(text="Clock").click()
button = d(className='android.widget.Button', text='Next')
button.click()

# swipe from (sx, sy) to (ex, ey)
d.swipe(sx, sy, ex, ey)

更详细的使用方法可参考项目文档

通过这种方式,我们就可以极大地简化 Python 操作 UI 控件的方式,不用再对照着UI Automator API使用 Java 编写测试用例,也不用再反复编译 jar 文件,省去了同一个测试场景需要维护两套代码的麻烦,整个过程也更加 Pythonic。

当然,pyuiautomator也并非完美,毕竟它不是 Google 官方维护的,从我实际使用的经历来看,有时候也会存在一些问题。这里先不展开,后续会单独写一篇博客。不过,总的来说,pyuiautomator还是非常值得使用的,它的确可以大大简化测试工作量。

现在继续本文的主题,我们怎么采用pyuiautomator来进行 UI 控件操作呢?

其实从上面的示例代码中就可以看到,UI 控件的操作分为两步,首先是对目标控件进行定位,然后是对定位的控件执行动作。

定位控件

在 UI 中,每个控件都有许多属性,例如classtextindex等等。我们要想对某一个控件进行操作,必然需要先对目标控件进行选择。

在上面的pyuiautomator用法示例中,已经包含了控件选择的代码:

checkbox = d(className='android.widget.CheckBox', checked='false')
button = d(className='android.widget.Button', text='Next')

在这两行代码中,分别实现了对 checkbox 和 button 的选择。基本原则就是,通过指定的控件属性,可以唯一确定目标控件。

那么,我们怎么知道目标控件有哪些属性,以及对应的属性值是什么呢?

Google 官方提供了两个工具,hierarchyvieweruiautomatorviewer,这两个工具都位于<android-sdk>/tools/目录下。关于这两个工具的区别及其各自的特点,本文不进行详细介绍,我们当前只需要知道,在查看控件属性方面,这两个工具实现的功能完全相同,界面也完全相同,我们任选其一即可。

通过这个工具,我们可以查看到当前设备屏幕中的 UI 元素信息:

需要强调的是,工具每执行一次 dump,获取到的 UI 信息仅限于当前屏幕中前端(foreground)显示的内容。

获得 UI 元素的信息后,由于 UI 控件是以树形结构进行存储,而且每个控件都存在 index 属性值,因此,理论上讲,通过层级结构和 index 属性就能唯一指定任意 UI 控件。

然而,这并不是最佳实践。因为通常情况下,UI 布局的树形结构层级较多,通过层级关系进行指定时会造成书写极为复杂,而且从代码中很难一眼看出指定的是哪个控件。不信?看下这个例子就能体会了。如下代码对应的就是上图中红色方框的控件,可以看出,要是寻找每个控件都要从顶级节点开始,要将根节点到目标控件的路径找出来,这也是一个很大的工作量,而且很容易出错。

d(className='android.widget.FrameLayout')
  .child(className='android.widget.LinearLayout')
  .child(className='android.widget.FrameLayout')
  .child(className='android.widget.FrameLayout')
  .child(className='android.widget.FrameLayout')
  .child(className='android.widget.FrameLayout')
  .child(className='android.widget.FrameLayout')
  .child(className='android.view.View')
  .child(className='android.widget.FrameLayout')
  .child(className='android.widget.FrameLayout')
  .child(className='android.view.View')
  .child(className='android.widget.FrameLayout')
  .child(className='android.widget.TextView')

在实际应用中,我们更多地是采用控件的属性信息来定位控件,一般情况下,采用属性值text就能唯一确定目标控件了。例如同样是对上图中的红色方框进行定位,如下代码就足够了。

d(text='UC头条有新消息,点击刷新')

不过,经常会出现目标控件的text属性值为空的情况,这个时候我们一般就会采用class属性和部分层级关系组合的方式。同样是上图中的红色方框,我们也可以使用如下方式进行定位。

d(className='android.widget.FrameLayout').child(className='android.widget.TextView')

可以看出,同一个控件,我们可以采用多种方式进行定位。具体选择何种定位方式,可以参考如下准则:

这里说到了定位方式的准确性,那要如何进行验证呢?技巧是,采用.count.info属性值即可。

>>> d(text='UC头条有新消息,点击刷新').count
1
>>> d(text='UC头条有新消息,点击刷新').info
{u'contentDescription': None, u'checked': False, u'clickable': True, u'scrollable': False, u'text': u'UC\u5934\u6761\u6709\u65b0\u6d88\u606f\uff0c\u70b9\u51fb\u5237\u65b0', u'packageName': u'com.UCMobile.projectscn1098RHEAD', u'selected': False, u'enabled': True, u'bounds': {u'top': 1064, u'left': 42, u'right': 1038, u'bottom': 1136}, u'className': u'android.widget.TextView', u'focusable': False, u'focused': False, u'checkable': False, u'resourceName': None, u'longClickable': False, u'visibleBounds': {u'top': 1064, u'left': 42, u'right': 1038, u'bottom': 1136}, u'childCount': 0}

.count属性值为 1,.info属性值的内容与目标控件的属性值一致时,就可以确定我们采用的定位方式是准确的。

控件操作

定位到具体的控件后,操作就比较容易了。

pyuiautomator中,对UI Automator的 UI 操作动作进行了封装,常用的操作动作有:

更多的操作可根据我们测试场景的实际需求,查询pyuiautomator文档找到合适的方法。

总结

看到这里,相信大家对 Android UI 自动化测试已经有了基本的认识。由于篇幅关系,我没有将所有内容都包含进来,而是打算后续分多个专题以单独博文的形式进行展开(不知不觉就给自己埋下了坑_)。

参考文档


关于作者

笔名九毫,英文名 Leo Lee。

专注于软件测试行业,享受在墙角安静地 debug,也喜欢在博客上分享文字。

个人博客:http://debugtalk.com


↙↙↙阅读原文可查看相关链接,并与作者交流