Appium 简单改造 appium ,实现 Android 平台不启动应用直接执行用例

陈恒捷 · 2015年09月20日 · 最后由 assless 回复于 2017年02月14日 · 3087 次阅读
本帖已被设为精华帖!

首先,大家都知道 appium 在 android 平台上的底层实现使用的是 UIAutomator 。而 UIAutomator 是具有应用无关的特性的,即不需要打开应用即可控制系统界面。然而 appium 默认 把启动应用与执行测试绑定在了一起,因此无法使用像 UIAutomator 那样应用无关的方式执行用例。

接下来,我会简单介绍一下 appium 在 android 平台初始化 session 的主要步骤,并告诉大家如何在修改数行代码后实现不启动应用直接执行用例。

测试脚本初始化 session 过程简单解析

考虑到这部分不是最重点的部分,因此用文字说明,方便大家理解。

此处的初始化 session 指脚本中 webdriver.Remote('http://localhost:4723/wd/hub,desired_caps') 这个初始化 driver 的语句。

  1. appium server 接收到来自 client 的启动 session 的请求(/wd/hub/session)
  2. appium server 通过解析请求信息,得到 platform,app,activity 等关键信息
  3. 判断目前是否已存在 session 以及 Override session 是否为 true 。如果不存在 session 或 override session 为 true ,继续执行。
  4. appium server 根据解析得到的数据,执行下面一系列操作
    1. 根据用户的 full reset 和 no rest 参数值确定是否需要卸载应用。默认使用 fast reset
    2. 根据 platform 类型获取对应的 configurate 来检查该平台对应模块是否有效。这也是为何直接在 github 上下载 zip 代码文件会跑不起来的原因。如果出错,会报 Trying to run a session for device 'android' but that device hasn't been configured. Run config 错误。
    3. 建立 device 实例。一个 device 实例对应一个特定平台(1.4.0 上有 iOS ,safari,android,chrome,Selendroid, firefoxOS 共 6 个平台)
    4. 调用 device 实例的 configure 方法进行完整的初始化 session 步骤。接下来的部分以 device 类型为 android 作为例子。
      1. 检查 java
      2. 初始化 appium-adb 模块(它会自己去根据环境变量找到 adb )
      3. 解析 apk 的 mainfest 获取 package 名称和 launch activity
      4. 初始化 uiautomator 模块
      5. 检查设备连接。如果是模拟器就会启动模拟器并等待直到模拟器启动完毕。
      6. 检查 api level 。如果 api level 低于 17 则提示不支持。
      7. 根据 apk 信息 push string.json 。目前没研究到这个动作有什么用处
      8. 根据 mainfest 获取 app 启动使用的 process
      9. 卸载应用(前提是 full rest 为 true)
      10. 安装被测应用
      11. 绑定 bootstrap 端口
      12. push bootstrap 脚本(一个特殊的 uiautomator 脚本,可以接收特定端口的命令并执行对应操作)
      13. 安装并配置 unicode keyboard
      14. 安装 setting.apk ,用于设置网络状态
      15. 安装 Unlock.apk,用于解锁
      16. 启动 uiautomator
      17. 唤醒设备
      18. 解锁设备
      19. 获取 data 目录路径
      20. 配置 dump 时是否使用 compress
      21. 启动被测应用
      22. 如果有 autoWebview,切换到 webview context
      23. 根据实际情况更新 capibility 。如 device name 改成是 udid ,platform version 改成是实际 version 。
      24. 返回 session id 给 client ,session 建立完成

从上面的步骤可以看到,和 uiautomator 相关的操作只有:

  1. 绑定 bootstrap 端口
  2. push bootstrap 脚本(一个特殊的 uiautomator 脚本,可以接收特定端口的命令并执行对应操作)
  3. 启动 uiautomator

而安装/启动被测应用则是不同的操作。两者耦合度很低,意味着我们可以通过去掉安装/启动的步骤来实现不启动应用完成 session 初始化。

动手改造

首先,上面提到的配置 android 平台的 session 的主要过程都在这个函数中:

lib/devices/android/android.js

...
Android.prototype.start = function (cb, onDie) {
  this.launchCb = cb;
  this.uiautomatorExitCb = onDie;
  logger.info("Starting android appium");
  async.series([
    this.initJavaVersion.bind(this),
    this.initAdb.bind(this),
    this.packageAndLaunchActivityFromManifest.bind(this),
    this.initUiautomator.bind(this),
    this.prepareDevice.bind(this),
    this.checkApiLevel.bind(this),
    this.pushStrings.bind(this),
    this.processFromManifest.bind(this),
    this.uninstallApp.bind(this),
    this.installAppForTest.bind(this),
    this.forwardPort.bind(this),
    this.pushAppium.bind(this),
    this.initUnicode.bind(this),
    this.pushSettingsApp.bind(this),
    this.pushUnlock.bind(this),
    function (cb) {this.uiautomator.start(cb);}.bind(this),
    this.wakeUp.bind(this),
    this.unlock.bind(this),
    this.getDataDir.bind(this),
    this.setupCompressedLayoutHierarchy.bind(this),
    this.startAppUnderTest.bind(this),
    this.initAutoWebview.bind(this),
    this.setActualCapabilities.bind(this)
  ], function (err) {
    if (err) {
      this.shutdown(function () {
        this.launchCb(err);
      }.bind(this));
    } else {
      this.didLaunch = true;
      this.launchCb(null, this.proxySessionId);
    }
  }.bind(this));
};

非常一目了然。注释掉和应用安装/启动相关的函数后,它会变成这个样子:

Android.prototype.start = function (cb, onDie) {
  this.launchCb = cb;
  this.uiautomatorExitCb = onDie;
  logger.info("Starting android appium");
  async.series([
    this.initJavaVersion.bind(this),
    this.initAdb.bind(this),
    //this.packageAndLaunchActivityFromManifest.bind(this),
    this.initUiautomator.bind(this),
    this.prepareDevice.bind(this),
    this.checkApiLevel.bind(this),
    //this.pushStrings.bind(this),
    //this.processFromManifest.bind(this),
    //this.uninstallApp.bind(this),
    //this.installAppForTest.bind(this),
    this.forwardPort.bind(this),
    this.pushAppium.bind(this),
    this.initUnicode.bind(this),
    this.pushSettingsApp.bind(this),
    this.pushUnlock.bind(this),
    function (cb) {this.uiautomator.start(cb);}.bind(this),
    this.wakeUp.bind(this),
    this.unlock.bind(this),
    this.getDataDir.bind(this),
    this.setupCompressedLayoutHierarchy.bind(this),
    //this.startAppUnderTest.bind(this),
    //this.initAutoWebview.bind(this),
    this.setActualCapabilities.bind(this)
  ], function (err) {
    if (err) {
      this.shutdown(function () {
        this.launchCb(err);
      }.bind(this));
    } else {
      this.didLaunch = true;
      this.launchCb(null, this.proxySessionId);
    }
  }.bind(this));
};

这个时候所有启动应用的相关操作都不会进行。 session 建立后我们仍然能通过 appium 测试脚本控制手机界面,绝大部分操作仍然能正常进行。

Demo

# -*- coding:utf-8 -*-
import os

from appium import webdriver

# Returns abs path relative to this file and not cwd
PATH = lambda p: os.path.abspath(
    os.path.join(os.path.dirname(__file__), p)
)

if __name__ == "__main__":
    desired_caps = {}
    desired_caps['platformName'] = 'Android'
    desired_caps['platformVersion'] = '4.4'
    desired_caps['deviceName'] = 'e4d42545'
    desired_caps['unicodeKeyboard'] = 'true'
    desired_caps['resetKeyboard'] = 'true'
    desired_caps['app'] = PATH(
        '../app/Dianping_dianping-web_7.1.1.apk'
    )
    driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
    print "Start driver success!"

    print "Trying to click browser icon"
    driver.find_element_by_xpath('//android.widget.TextView[@text="Browser"]').click()
    print "Finish clicking browser icon. The browser should be opened."

    driver.quit()

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 38 条回复 时间 点赞

沙发,好牛逼

#1 楼 @monkey 好快。。。
这个一点都不牛逼啊。完整地做还得加上对应的 server argument 和 desired_caps,以及去掉 quit 时点击 home 键的操作 。我这个只是 demo 级别的东西。。。

我记得以前有个命令行参数是可以支持不重新启动 app 的. 后来修改的貌似就慢慢丢掉这个 feature 了.

#3 楼 @seveniruby 是吗?我主要从 1.2.0 开始接触,以前的没细究。
虽然会用到的人不多,但我觉得有这个功能还是挺不错的。

我觉得这个帖子中测试脚本初始化 session 过程简单解析部分就是精华了...

#5 楼 @anikikun 哈哈,其实真的简化了好多东西。详细版我以前写过一小部分,后面写得太累没继续写了(真的逐行代码研究的话挺复杂的)。有兴趣可以看看:Appium 学习笔记(3)- Appium 建立 session 全过程(Android Native)(1)

#7 楼 @chenhengjie123 个人博客时代已经终结。用 wordpress 不如在 github 上写

看到这个我首先想到的是有没有办法从外部去实现,毕竟改了代码后就不能让它自动帮你初始化了,当然你可以再将 android.js 改来改去又或者是自己重复造轮子来完成初始化。我想到的最简单粗暴的办法就是:直接启动一个启动后就关闭的应用,然而 appium 的 io.appium.unlock 自带这个属性。初始化过程分析得很好,学习了。另外那个 UiAutomator 的 jar 让我又想到可以自己再造一些轮子了,又或者自己再写一个支持交互的 jar 再造一些方的轮子什么的。。。

绝对是精华,学习了!

#8 楼 @lihuazhang 嗯,改天看看 github 能不能满足我需要。主要是访问 github 速度好慢。。。

好爽哦这个东西

看到这篇文档,说说我的理解.

文件:

ib\appium.js

函数:

Appium.prototype.invoke = function (cb)
if (this.device.args.autoLaunch === false) {
   // if user has passed in desiredCaps.autoLaunch = false
   // meaning they will manage app install / launching
   if (typeof this.device.noLaunchSetup === "function") {
     this.device.noLaunchSetup(function (err) {
       if (err) return cb(err);
       cb(null, this.device);
     }.bind(this));
   } else {
     cb(null, this.device);
   }
 } else {
   // the normal case, where we launch the device for folks
   var onStart = function (err, sessionIdOverride) {
     if (sessionIdOverride) {
       this.sessionId = sessionIdOverride;
       logger.debug("Overriding session id with " +
                   JSON.stringify(sessionIdOverride));
     }
     if (err) return this.cleanupSession(err, cb);
     logger.debug("Device launched! Ready for commands");
     this.setCommandTimeout(this.desiredCapabilities.newCommandTimeout);
     cb(null, this.device);
   }.bind(this);

   this.device.start(onStart, _.once(this.cleanupSession.bind(this)));
 }

这个函数会判断 autoLaunch 参数,直接将判断部分代码注释掉。只留下

var onStart = function (err, sessionIdOverride) {
    if (sessionIdOverride) {
      this.sessionId = sessionIdOverride;
      logger.debug("Overriding session id with " +
      JSON.stringify(sessionIdOverride));
    }
    if (err) return this.cleanupSession(err, cb);
    logger.debug("Device launched! Ready for commands");
    this.setCommandTimeout(this.desiredCapabilities.newCommandTimeout);
    cb(null, this.device);
  }.bind(this);

  this.device.start(onStart, _.once(this.cleanupSession.bind(this)));

在回放脚本时 不设置 app 的参数,autoLaunch = false,就可以不启动 APP,直接执行脚本中的动作。

#13 楼 @kilmer 这个你试过确实能达到和上面 Demo 一样的效果吗?如果能的话这个方法也不错。

java 的应该是在

AndroidDriver.java 下的 startActivity

这个方法吧,但里面的操作的方法有点看不懂

#15 楼 @darker50 你指的是 Java-client 的?

#18 楼 @kilmer 好方法!回头我测试过后加到帖子正文中。

学习

21楼 已删除

试过了,怎么不行,还要启动

#22 楼 @Josen 你具体的是哪个文件?要改实际用的那个哦。改源码的话不重新 build 是不会生效的。

#23 楼 @chenhengjie123 lib/devices/android/android.js 就你这个啊,改好以后重新启动的 appium 客户端,还是会启动 app,执行对应操作。

#24 楼 @Josen 你用的 appium 版本是?

不清楚不启动应用直接执行用例的目的是什么?什么场景用?难道是 precondition?

#26 楼 @softblank 例如要自动化系统自带应用

#28 楼 @Josen
确定下你启动的 appium 客户端是不是确实就是你修改的这个?

确定方法:往修改的文件里加一个日志,检查 appium 跑到这个位置的时候是不是确实有这个日志输出。

#29 楼 @chenhengjie123 确定是。不过我用的是真机测试,没关系吧。

#30 楼 @Josen 没关系的。

方便把你修改的这部分代码贴上来不?还有具体修改的文件路径。

#31 楼 @chenhengjie123 楼主,我你这个方法是针对 Appium GUI 端的吗?命令行安装的 appium 里面没有 android.js 文件,这个有什么办法吗?

#32 楼 @yufanW 你用的是什么版本?不排除新版本重构后文件名或者文件位置改了。

#33 楼 @chenhengjie123 我用的是 appium 1.6.1 的,我电脑上还装了一个 1.4.16 的 GUI 版本的,确实路径里面有 android.js,但项目需要在命令行版本上跑,所以求助。。

#34 楼 @yufanW 1.6.1 重构后 android.js 的内容应该放到了 appium-android-driver(大概是这个名字)这个单独的 node 模块了,你可以到 node_modules 这个 文件夹里面找下。

#35 楼 @chenhengjie123 找到了,多谢~

#23 楼 @chenhengjie123 加个 QQ 问,865349382

#37 楼 @Josen 额,你直接发帖问吧,qq 上回答技术问题不方便。

启动 appium 没那么麻烦,写个 bat 脚本就可以了:
脚本先查找有没有占用的端口,然后杀掉占用端口的进程。
最后用命令行启动相关的脚本,路径跟自己 appium 安装的路径相关,需要自行调整一下。

@echo off
netstat -ano|findstr 4723|findstr 0.0.0.0:0 > D:\Tools\bat\tmp.txt
for /f "tokens=5" %%i in (D:\Tools\bat\tmp.txt) do taskkill /PID %%i -t -f
node "D:\Program Files (x86)\Appium\node_modules\appium\lib\server\main.js" --address 127.0.0.1 --port 4723 --platform-name Android --platform-version 23 --automation-name Appium --log-no-color 

然后你程序调用这个脚本就可以了,启动的命令你可以在 appium 启动的之后的界面上找到:

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册