背景

需要实现跑步性能测试,我们的实现流程是安卓开发单写了一个类似于小熊快跑的app,这个app的唯一功能是解析gps文件,然后快速模拟定位,然后打开某APP跑步模块,就可以模拟跑步了,由于在release版本内关闭了模拟定位的功能,只在debug包内支持模拟定位跑步的功能。这样实现流程就简单了。

graph TD
安装模拟GPSAPP-->传入GPS文件
传入GPS文件-->启动APP跑步模块
启动APP跑步模块-->记录手机端的性能数据
记录手机端的性能数据-->分析报告

graph TD
安装模拟GPSAPP-->传入GPS文件
传入GPS文件-->启动APP跑步模块
启动APP跑步模块-->记录手机端的性能数据
记录手机端的性能数据-->分析报告
关键点

里面涉及到几个关键实现的点,我一一列举一下。

1、启动模拟跑步APP和APP跑步模块

很久之前,写过Appium的自动化测试,但是这次准备用atx。

因为之前写iOS UI自动化的时候接触过atx,atx即封装的webdriveragent,和uiautomator。

pip install --upgrade --pre atx --user -U

pip install opencv_contrib_python --user -U


pip install --upgrade --pre atx --user -U

pip install opencv_contrib_python --user -U

官方文档提供了一些简便的api,由于我只当前只关注Android,其实我对iOS更熟悉,因为之前在研究python-wda源码的时候也有过一点点的贡献。

话不多说,上代码。

首先,在安装好atx以后可以检查一下环境和当前的版本:

python -m atx version
# 检查环境配置是否正常
python -m atx doctor

python -m atx version
# 检查环境配置是否正常
python -m atx doctor

还好,都是正常的。

这个时候,我从Jenkins构建拿到了两个APP,分别是APPDebug版本和模拟跑步APP。

先用atx支持的命令取得app的包名和启动activity名字

举其中GPS.apk的例子。

python -m atx apkparse GPS.apk

python -m atx apkparse GPS.apk
{
    "version": {
        "code": "1", 
        "name": "1.0"
    }, 
    "main_activity": "com.FakeGPXActivity", 
    "package_name": "com.fakegps"
}

{
    "version": {
        "code": "1", 
        "name": "1.0"
    }, 
    "main_activity": "com.FakeGPXActivity", 
    "package_name": "com.fakegps"
}

取到packagename和activity之后,就可以启动app然后进行一系列的操作了。

## 启动模拟 GPS数据app

import atx

self.driver = atx.connect()
self.driver.start_app("com.fakegps", "com.FakeGPXActivity")


## 启动模拟 GPS数据app

import atx

self.driver = atx.connect()
self.driver.start_app("com.fakegps", "com.FakeGPXActivity")
2、获取手机端的内存和PSS值

对于如何获取手机端的性能数据,testerhome里面有很多这方面知识的描述,我简单描述最后如何放在启动APP和执行点击操作的调用。

是通过异步的方式,先在启动APP前,初始化获取数据的线程,然后在执行操作的命令前,结束数据的获取。

因为在给Flask增加单元测试中,当时接触过Python的几个测试框架,对Nose最有好感,最后用例的编写是通过Nose,启动,直接通过nosetests即可。

import atx
## 这里是封装的取Android性能数据的部分代码
from cpu_mem_log_thread import CpuMemLogThread

packagename = 'com.xx'
activity = 'com.xx'
packagename_gps = 'com.xxx'
activity_gps = 'com.xxx'
filename = 'perftest.log'


class test():
    def setup(self):
        self.driver = atx.connect()
        self.driver.start_app(packagename_gps, activity_gps)
        self.driver.sleep(5)
        self.driver.start_app(packagename, activity)
        self.log_thread = CpuMemLogThread(packagename, filename)
        self.log_thread.start()

    def teardown(self):
        self.exit_run_module()
        self.log_thread.stop = True

        self.driver.stop_app(packagename)
        self.driver.stop_app(packagename_gps)

    def exit_run_module(self):
        self.driver.click_exists(resourceId='running_button_change_mode_to_normal')

        self.driver(resourceId='pause_button').long_click()

        if self.driver(resourceId='finish_button').exists:
            self.driver(resourceId='finish_button').click()
        else:
            raise Exception(u"出错了,没有结束按钮")

        # print self.driver.dump_view()
        self.driver(text=u'确定').click()

    def test_run(self):
        self.driver.sleep(5)

        self.driver.click_exists(text=u'运动')
        self.driver.sleep(2)
        self.driver.click_exists(text=u'继续')
        self.driver.sleep(5)
        if self.driver(resourceId='running_button_change_mode_to_normal').exists:
            self.driver.click_exists(resourceId='running_button_change_mode_to_normal')
        else:
            raise Exception(u"出错了,不在跑步地图界面")

        # 这里我们默认让app模拟跑3个小时
        self.driver.sleep(10800)
        self.driver.screenshot('run_end.png')

import atx
## 这里是封装的取Android性能数据的部分代码
from cpu_mem_log_thread import CpuMemLogThread

packagename = 'com.xx'
activity = 'com.xx'
packagename_gps = 'com.xxx'
activity_gps = 'com.xxx'
filename = 'perftest.log'


class test():
    def setup(self):
        self.driver = atx.connect()
        self.driver.start_app(packagename_gps, activity_gps)
        self.driver.sleep(5)
        self.driver.start_app(packagename, activity)
        self.log_thread = CpuMemLogThread(packagename, filename)
        self.log_thread.start()

    def teardown(self):
        self.exit_run_module()
        self.log_thread.stop = True

        self.driver.stop_app(packagename)
        self.driver.stop_app(packagename_gps)

    def exit_run_module(self):
        self.driver.click_exists(resourceId='running_button_change_mode_to_normal')

        self.driver(resourceId='pause_button').long_click()

        if self.driver(resourceId='finish_button').exists:
            self.driver(resourceId='finish_button').click()
        else:
            raise Exception(u"出错了,没有结束按钮")

        # print self.driver.dump_view()
        self.driver(text=u'确定').click()

    def test_run(self):
        self.driver.sleep(5)

        self.driver.click_exists(text=u'运动')
        self.driver.sleep(2)
        self.driver.click_exists(text=u'继续')
        self.driver.sleep(5)
        if self.driver(resourceId='running_button_change_mode_to_normal').exists:
            self.driver.click_exists(resourceId='running_button_change_mode_to_normal')
        else:
            raise Exception(u"出错了,不在跑步地图界面")

        # 这里我们默认让app模拟跑3个小时
        self.driver.sleep(10800)
        self.driver.screenshot('run_end.png')

nose执行的时候会先调用执行setup,在用例结束执行之后,会调用teardown,所以把setup中放入了启动APP应用和开始执行性能数据抓取的动作,而在teardown中放入先停止执行性能数据抓取操作,最后stop app之前启动的APP。

具体的执行命令是:

nosetests -v run.py --with-xunit

nosetests -v run.py --with-xunit
3、跑步性能测试的报告

单独配置了一个html模板,然后解析nosetests.xml,加上截屏和perf.log,作为数据参数。

然后通过jinja2传入html模板,然后生成报告,最后报告的样式如下:

Android 仿跑步app 手机模拟跑步场景软件_Android 仿跑步app

后续

当我真正把这套流程部署在Jenkins上的slave以后,总是会出现,手机和mac连接不稳定的情况出现,当我都要放弃的时候,@codeskyblue提出已经升级了uiautomator2, 运行更加稳定同时也支持无线连接,也的确不在想重新配置appium环境,就用uiautomator2去解决atx稳定执行的问题

使用新的uiautomator2库

配置环境

git clone https://github.com/openatx/uiautomator2
cd uiautomator2

# 用当前用户权限安装
python setup.py install --user --prefix=

git clone https://github.com/openatx/uiautomator2
cd uiautomator2

# 用当前用户权限安装
python setup.py install --user --prefix=

从https://github.com/openatx/atx-agent/releaseslinux_armv7.tar.gz下载以 结尾的二进制包。绝大部分手机都是linux-arm架构的。

解压出atx-agent文件,然后打开控制台

$ adb push atx-agent /data/local/tmp
$ adb shell chmod 755 /data/local/tmp/atx-agent
# launch atx-agent in daemon mode
$ adb shell /data/local/tmp/atx-agent -d


$ adb push atx-agent /data/local/tmp
$ adb shell chmod 755 /data/local/tmp/atx-agent
# launch atx-agent in daemon mode
$ adb shell /data/local/tmp/atx-agent -d

这里的输出一定要是 0.0.3,再继续。若有问题,建议可以adb shell进去对atx-agent执行chmod授权操作。

由于我的手机ip和mac的网络不在同一个网段,因而需要一个adb forward转发操作,当然也可以直接启动手机端的ip,不进行转发操作。

adb forward tcp:7912 tcp:7912

adb forward tcp:7912 tcp:7912

这个时候分别执行

adb shell 'echo $(curl -s localhost:7912/version)'

curl localhost:7912/version

adb shell 'echo $(curl -s localhost:7912/version)'

curl localhost:7912/version

0.0.3,说明环境已经配置好了,这个时候更新一下之前代码:

import uiautomator2
import time
from cpu_mem_log_thread import CpuMemLogThread

packagename = 'com.xx'
activity = 'com.xx'
packagename_gps = 'com.xxx'
activity_gps = 'com.xxx'
filename = 'perftest.log'


class test():
    def setup(self):

        self.driver = uiautomator2.connect('http://localhost:7912')
        self.driver.app_start(packagename_gps, activity=activity_gps)
        time.sleep(5)
        self.driver.app_start(packagename, activity=activity)
        self.log_thread = CpuMemLogThread(packagename, filename)
        self.log_thread.start()

    def teardown(self):
        self.exit_run_module()
        self.log_thread.stop = True

        self.driver.app_stop(packagename)
        self.driver.app_stop(packagename_gps)

    def exit_run_module(self):
        self.driver(resourceId='running_button_change_mode_to_normal').click()

        self.driver(resourceId='pause_button').long_click()

        if self.driver(resourceId='finish_button').exists:
            self.driver(resourceId='finish_button').click()
        else:
            raise Exception(u"出错了,没有结束按钮")

        # print self.driver.dump_view()
        self.driver(text=u'确定').click()

    def test_run(self):
        time.sleep(5)
        self.driver(text=u'运动').click()
        self.driver(text=u'继续').click()
        if self.driver(resourceId='running_button_change_mode_to_normal').exists:
            self.driver(resourceId='running_button_change_mode_to_normal').click()
        else:
            raise Exception(u"出错了,不在跑步地图界面")

        # 这里我们默认让app模拟跑3个小时
        self.driver.sleep(10800)
        self.driver.screenshot('run_end.png')

import uiautomator2
import time
from cpu_mem_log_thread import CpuMemLogThread

packagename = 'com.xx'
activity = 'com.xx'
packagename_gps = 'com.xxx'
activity_gps = 'com.xxx'
filename = 'perftest.log'


class test():
    def setup(self):

        self.driver = uiautomator2.connect('http://localhost:7912')
        self.driver.app_start(packagename_gps, activity=activity_gps)
        time.sleep(5)
        self.driver.app_start(packagename, activity=activity)
        self.log_thread = CpuMemLogThread(packagename, filename)
        self.log_thread.start()

    def teardown(self):
        self.exit_run_module()
        self.log_thread.stop = True

        self.driver.app_stop(packagename)
        self.driver.app_stop(packagename_gps)

    def exit_run_module(self):
        self.driver(resourceId='running_button_change_mode_to_normal').click()

        self.driver(resourceId='pause_button').long_click()

        if self.driver(resourceId='finish_button').exists:
            self.driver(resourceId='finish_button').click()
        else:
            raise Exception(u"出错了,没有结束按钮")

        # print self.driver.dump_view()
        self.driver(text=u'确定').click()

    def test_run(self):
        time.sleep(5)
        self.driver(text=u'运动').click()
        self.driver(text=u'继续').click()
        if self.driver(resourceId='running_button_change_mode_to_normal').exists:
            self.driver(resourceId='running_button_change_mode_to_normal').click()
        else:
            raise Exception(u"出错了,不在跑步地图界面")

        # 这里我们默认让app模拟跑3个小时
        self.driver.sleep(10800)
        self.driver.screenshot('run_end.png')

可以看到运行明显稳定太多,而且api基本也没有变化。

由于uiautomator2库并不完善,之前还不支持activity和packagename启动,我也提了一个特别简单的pr,可以支持activity和packagename启动。

最后 感谢@codeskyblue,他的分享,让我们可以这么轻便的去实现简单的页面点击操作的需求。

一直在探索UI自动化实现性价比高的方式,因为我们都知道,UI自动化投入产出比是不高的。

但是我们需要找到一些特别的方式,比如某个模块,而且人工真的不好经常重复操作的行为,感觉这才是UI自动化可以有突破的点。

https://testerhome.com/topics/10298