首先,大家都知道 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()


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