今天晚上花了点时间继续研究了一下 appium 的源码和相关资料,结合之前群里一直讨论的 appium 如何支持 hybrid 页面测试的大话题,输出一个新的折腾结果:webview36 支持的 Remote Debugging 的应用技巧,观点错误之处,还望及时指出,知道的同学也可以继续参与讨论。

注意:本文讨论的是 4.4 以上的新 webview,欢迎有同学另发帖子,讨论 selendroid,作为一枚前端开发来说,是希望看到 webview 逐步进化的,因为这是必然趋势,h5 和 js 在手机上的性能会越来越强劲。

本文主要介绍以下几个内容:

  1. Remote Debugging on Android with Chrome
  2. Appium 的 android-hybrid.js 以及 chromedriver.js 部分源码分析
  3. chromedriver latest release 对 hybrid 测试的支持参数

demo 环境:
Desktop chrome ver: 39.0.2171.95 (64-bit)
chromedriver: 2.13.307650
app:ctrip app
Android: 4.4.2

再次认识一下 Hybrid

Hybrid 的直译为:杂交,当然了,我们忽略他原来的意思,这个词语本身在 app 开发中非常常见,它代表一种开发模式,即在 app 端采用原生 (native) 技术,一般为 oc,java 等语言 + web 页面开发技术 (一般为 html5,javascript 等相关技术) 进行融合,在适当的环节采用适当的实现方式,能够使开发效率达到一个满意值,并且产品的交互等各方面表现能够接近原生,国内像携程,大众点评网的 app 等均采用了这种模式,那么这个时候问题来了,这些非原生技术实现的页面的载体是什么?相信很多同学都知道,是 Webview,ok,现在我们知道,很多时候 Hybrid 的那些页面都是通过这样一种载体进行展示的,暂且不深入讨论 webview,webview 正在不断进化,这是我们希望看到的。

注意:区分 webkit webview 和 chrome 内核的 webview

一些周边帖:《chrome mobile emulation 及周边漫谈和相关应用 + 想法》
http://testerhome.com/topics/1697

自动化测试在遭遇 Hybrid 时的窘境

其实,我这里用"窘境"这个词语非常不恰当,因为,当你发现其实这些问题都存在解决方案的时候,你就会发现其实这不算什么,相反,官方支持地很好,而且也正在不断进化。

Remote Debugging on Android with Chrome

官方文档及说明:

The way your web content behaves on mobile can be dramatically different from the desktop experience. Remote debugging with Chrome DevTools lets you debug live content on your Android device from your development machine.

使用 Chrome 的开发者工具能够让你即时调试在设备上运行的页面,他们是:

Debugging websites in browser tabs.

重点来了:Debugging WebViews in native Android apps. 调试在安卓应用里的 webviews
好像是我们想要的......

Screencasting live to your development machine from your Android device.
令人兴奋的 casting 特性

Accessing your development server on Android using port forwarding and virtual host mapping.
端口映射调试模式,暂时不用管。

继续过滤出来我们关注的信息:

要求

To begin remote debugging, you need:

Chrome 32 or later installed on your development machine.
Chrome 32 + 以上版本(本机)

A USB cable to connect your Android device.
USB 线

下面两行又是重点,Appium 对 webview 测试支持的部分策略就跟这个有很大联系
For browser debugging: Android 4.0+ and Chrome for Android.
For app debugging: Android 4.4+ and a WebView configured for debugging.
对于应用调试,4.4 以上

Note: Remote debugging requires your version of desktop Chrome to be newer than the version of Chrome for Android on your device. For best results, use Chrome Canary (Mac/Windows) or the Chrome Dev channel release (Linux) on desktop.
推荐安装的桌面版 chrome 版本

打开我们的 chrome 开发者工具,我们来玩一把。

环境准备:
真机一个,或者模拟器一个,按照上述官方要求的 chrome 版本一个。
开启设备的 USB 调试模式
安装一个被测 apk,这里以携程为例:

在地址栏敲入 chrome://inspect

我们来到一个干货界面,不出意外,你将看到下面这张图:

我的模拟器界面:

当我们在 app 上进行操作时,开发者工具自动会将嗅探到的 webview 给我们列出来。
这个技能可以帮助测试人员自己查看内部元素。

我们只需要按下 inspect

我们看到,webview 里加载页面的源码信息以及 dom 结构已经完全展现在我们面前,而且你可以将鼠标停留在某个 dom 上,正如你所熟悉的开发者工具,它会帮你在 app 里面高亮显示出对应的元素。

更多调试技巧可以参考这里:https://developer.chrome.com/devtools/docs/remote-debugging

与 appium 的联系

这里,我只讨论安卓端,在 appium 的源码中,与这个实现有关系的两个源码文件是https://github.com/appium/appium/blob/master/lib/devices/android/android-hybrid.js
还有一个是
https://github.com/appium/appium/blob/master/lib/devices/android/chromedriver.js

我们先来看 android-hybrid.js

androidHybrid.listWebviews = function (cb) {
  logger.debug("Getting a list of available webviews");
  var webviews = [];
  var definedDeviceSocket = this.args.androidDeviceSocket;
  this.adb.shell("cat /proc/net/unix", function (err, out) {
    if (err) return cb(err);
    _.each(out.split("\n"), function (line) {
      line = line.trim();
      var webviewPid = line.match(/@?webview_devtools_remote_(\d+)/);
      if (definedDeviceSocket) {
        if (line.indexOf("@" + definedDeviceSocket) ===
          line.length - definedDeviceSocket.length - 1) {
          if (webviewPid) {
            webviews.push(this.WEBVIEW_BASE + webviewPid[1]);
          } else {
            webviews.push(this.CHROMIUM_WIN);
          }
        }
      } else if (webviewPid) {
        // for multiple webviews a list of 'WEBVIEW_<index>' will be returned
        // where <index> is zero based (same is in selendroid)
        webviews.push(this.WEBVIEW_BASE + webviewPid[1]);
      }
    }.bind(this));
    webviews = _.uniq(webviews);

    //...... 后面代码省略

相信很多同学也都看过源码了,client 端的 get contexts 方法对应服务端的这个 listWebviews,这个方法非常讨巧,我们清晰地看到,它执行了一条命令,然后利用正则表达式去匹配 webview_devtools_remote,见下图:

cat /proc/net/unix

webview_devtools_remote?似曾相识的感觉:
appium 的这个策略正是利用了 remote debugging 特性。

目前为止,我们只是能够嗅探到 webview 内部的元素,appium 接下来会怎么做?
我们应该一下子就能够想到 webdriver,所以 Appium 很自然地对其进行了包装:

androidHybrid.startChromedriverProxy = function (context, cb) {
  logger.debug("Connecting to chrome-backed webview");
  if (this.chromedriver !== null) {
    return cb(new Error("We already have a chromedriver instance running"));
  }

  var setupNewChromeDriver = function (ocb) {
    var chromeArgs = {
      port: this.args.chromeDriverPort
    , executable: this.args.chromedriverExecutable
    , deviceId: this.adb.curDeviceId
    , enablePerformanceLogging: this.args.enablePerformanceLogging
    };
    this.chromedriver = new Chromedriver(chromeArgs, this.onChromedriverExit.bind(this));
    this.rememberProxyState();
    this.proxyHost = this.chromedriver.proxyHost;
    this.proxyPort = this.chromedriver.proxyPort;
    this.isProxy = true;
    var caps = {
      chromeOptions: {
        androidPackage: this.args.appPackage, //貌似有戏...
        androidUseRunningApp: true //貌似有戏...
      }
    };

两个核心参数:

chromeOptions: {
        androidPackage: this.args.appPackage, 
        androidUseRunningApp: true
      }

chromedriver 官网对这两个参数的解释:

Android-specific Desired Capabilities

The following capabilities are applicable to both Chrome and WebView apps:

androidPackage: The package name of the Chrome or WebView app.
androidDeviceSerial: (Optional) The device serial number on which to launch the app (See Multiple Devices section below).
androidUseRunningApp: (Optional) Attach to an already-running app instead of launching the app with a clear data directory.

The following capabilities are only applicable to WebView apps.
androidActivity: Name of the Activity hosting the WebView.
androidProcess: (Optional) Process name of the Activity hosting the WebView (as given by ps). If not given, the process name is assumed to be the same as androidPackage.

尝试直接使用这几个参数

先开启一个 wd/hub 模式的 chromedriver

chromedriver --url-base=wd/hub

简短 demo

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from appium import webdriver


if __name__ == "__main__":

    capabilities = {
        'chromeOptions': {
            'androidPackage': 'ctrip.android.view',
            'androidUseRunningApp': True,
            'androidActivity': 'ctrip.android.view.h5.activity.H5Container'
        }
    }

    hybrid_test_driver = webdriver.Remote("http://127.0.0.1:9515/wd/hub", capabilities)
    a = hybrid_test_driver.find_element_by_id("div_czb")
    a.click()

注意:这里的 androidActivity 必须传入,可以用 monitor 等轻松嗅探到。

运行结果

新 webview 特性支持表

https://developer.chrome.com/multidevice/webview/overview

说明

本文就 webview 的测试没有讨论全面,比如 appium 的向下兼容处理(selendroid)策略,我们的本意是希望 webview 的进化越来越给力,这样方便的就是广大开发者和 testing skill 研究人员。

总结

对未来充满无限展望,我们在新 webview 特性的光环笼罩下,使用 chrome dev tools + chromedriver 的几个参数就能完成 webview 内的元素嗅探以及 driver 操作,我们也希望 webview 能够继续进化,让那些该死的兼容问题见鬼去吧。


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