Appium的使用

Appium 是一个跨平台的移动端自动化测试工具,可以非常便捷地为 iOS 和 Android 平台创建自动化测试用例。它可以模拟 App 的各种操作,如点击、滑动、文本输入等,我们手工能完成的操作 Appium 也都能完成。我们在第 7 章曾了解过 Selenium,这是一个网页端的自动化测试工具,Appium 实际上就类似于它,也是利用 WebDriver 实现自动化测试。对于 iOS 设备,Appium 使用 UIAutomation 实现驱动;对于 Android 设备,使用 UiAutomator 和 Selendroid 实现驱动。

Appium 提供了一个服务器,我们可以向这个服务器发送一些操作指令,然后 Appium 会根据不同的指令驱动移动设备完成不同的动作。

爬虫使用 Selenium 爬取 JavaScript 渲染的页面,实现所见即所爬。Appium 同样可以,所以在某些情况下,用 Appium 做 App 爬虫不失为一个好的选择。

本节我们就来了解 Appium 的基本使用方法,学习利用 Appium 进行自动化爬取的基本操作,主要目的是了解利用 Appium 进行自动化测试的流程以及相关 API 的用法。

准备工作

请确保已经做好如下准备工作。

  • 在电脑上安装好 Appium 客户端,并且客户端可以正常运行。

  • 在电脑上配置好 Android 开发环境并能正常使用 adb 命令。

  • 安装好 Python 版本的 Appium API。

以上 Appium 环境的具体配置方法可以参考 https://setup.scrape.center/appium。

除了配置好环境,还需要做到下面两步。

  • 准备一部 Android 真机或启动一个 Android 模拟器,并在上面安装好示例 App,下载地址为 https://app5.scrape.center/

  • 用 USB 线连接电脑和 Android 真机或模拟器,确保 adb 能够正常连接到 Android 真机或模拟器。

Appium 启动 APP

Appium 启动 App 的方式有两种:一种是用 Appium 内置的驱动器打开 App,另一种是利用 Python 代码打开 App。下面我们分别进行说明。

两种方法都需要启动 Appium 服务,因此先打开 Appium,启动界面如图 12-42 所示。

图12-42 Appium 的启动界面

直接点击 Start Server v1.15.1 按钮即可启动 Appium 服务,这就相当于开启了一个 Appium 服务器我们可以通过 Appium 内置的驱动器或 Python 代码向 Appium 服务发送一系列操作指令,它会根据不同的指令驱动移动设备完成不同的动作。Appium 启动后的运行界面如图 12-43 所示。

图12-43 Appium 启动后的运行界面

Appium 正在监听 4723 端口,我们向此端口对应的服务接口发送操作指令,运行界面就会显示这个过程的操作日志。可以输入 adb 命令测试和手机的连接情况,命令如下:

adb devices -l

如果输出结果类似下面这样,说明电脑已经正确连接手机:

List of devices attached
R5CN30RMoQLdeviceusb:338690048Xproduct:y2qzcxmodel:SM_G9860device:y2qtransport_id:1

其中 model 是设备的名称,就是即将会介绍到的 deviceName,对于不同的手机其结果不同,请以自己的结果为准。

这步一定是成功获取了设备信息才能证明电脑和手机连接成功了。如果提示找不到 adb 命 令,那么请检查 Android 开发环境和环境变量是否配置成功。如果可以成功调用 adb 命令但不显示设备信息,那么请检查手机和电脑的连接情况,如 USB 调试功能是否开启等。

接下来用 Appium 内置的驱动器打开 App,点击运行界面右上方的 Start Inspector Session 按钮,如图 12-44 所示。

图 12-44 操作示例

会打开一个配置页面,如图 12-45 所示。

我们需要在这里配置启动 App 时的 DesiredCapabilities 参数,包括 platformNamedeviceNameappPackageappActivity

  • platformName:平台名称,取值有 Android 和 ioS,此处填写 Android。

  • deviceName:设备名称,是手机的具体类型,即刚才获取的 model 值。

  • appPackage:App 包名,示例 App 的包名为 com.goldze.mvvmhabit

  • appActivity:入口 Activity 名,示例 App 的入口 Activity 名为 .ui.MainActivity

  • noReset:不重置 App 的状态,此处需要填 true,如果不填,那么每次打开 App 都和新安装时一样。例如启动了微信,而此参数没有设置,就会变成未登录状态。

图12-45 配置页面

当前配置页面的左下角链接了包含更多配置参数相关说明的文档,大家可以参考盖文档配置更多的参数。

不同 App 的 appPackage.appActivity 是不一样的,如果不知道它们的值,可以用 jadx(https:// github.com/skylot/jadx)这样的工具解析 App 安装包中的 AndroidManifest.xml 文件获取。例如这里用 jadx 工具打开示例 App,就可以看到它的 AndroidManifest.xml 文件,如图 12-46 所示。

图12-46 app5 的 AndroidManifest.xml 文件

appPackage 的值就是 manifest 根节点的 package 属性,这里它的值就是 com.goldze.mvvmhabit,如图 12-47 所示。

图12-47 manifest 根节点的 package 属性值

可以看到,AndroidManifest.xml 文件中有很多个 activity 节点,其中一个包含如下关键内容:

<intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>

这4行内容代表 app5 在启动时会启动该 activity 节点中声明的 Activity 对应的页面,找到该 activity 节点中的 android:name 属性值,这里就是 com.goldze.mvvmhabit.ui.MainActivity,如图 12-48 所示。

图12-48 activity 节点中的 android:name 属性

在 Appium 配置的时候,需要去掉属性值里前面的 appPackage 信息,于是结果为 .ui.MainActivity。接下来在 Appium 中加入上面 5 个配置,如图 12-49 所示。

图12-49 配置信息

点击 SaveAs…​ 按钮将配置信息保存下来,以后就可以继续使用这个配置。然后点击 StartSession 按钮,即可启动 Android 手机上的 App 并进入启动页面。同时,电脑端会弹出一个调试窗口,我们可以从这个窗口预览当前的手机页面,以及查看页面源代码,如图 12-50 所示。

图12-50 电脑端弹出的调试窗口

点击左栏中手机页面的某个元素,它就会高亮显示,如这里的电影名称 “霸王别姬”。这时中间栏会显示它对应的源代码;右栏会显示它的基本信息,如 indexclasstext 等。我们还可以在右栏执行一些操作,如 Tap、SendKeys、Clear,现在点击右栏的 Tap 按钮,即执行点击操作,如图 12-51 所示。

图12-51 点击 Tap 按钮

可以发现电脑端的页面发生了变化,如图 12-52 所示。

图12-52 电脑端的页面发生变化

左栏的手机页面跳转到了《霸王别姬》电影的详情页,中间栏的 Source 面板显示了当前页面的节点信息。那怎么返回呢?点击中间栏上方的 Back 按钮,如图 12-53 所示。

这时就返回首页了。Appium 还提供了动作录制功能,如图 12-54 所示,点击中间栏上方的 Start Recording 按钮,Appium 就会开始录制,之后我们在窗口中操作 App 的行为都会被记录下来,Recorde 面板中可以自动生成指定语言编写的代码。

图12-53 点击 Back 按钮返回

图12-54 点击 StartRecording 按钮录制动作

例如,点击 StartRecoding 按钮后,选中电影条目 “初恋这件小事”,然后点击 Tap 按钮,再点击返回,可以看到 Appium 的 Recorder 面板中出现了这些过程的操作代码,如图 12-55 所示。

这里我们选择的语言是 Python,代码逻辑是选中某个节点然后执行点击操作,接着返回,和我们手工操作的内容一一对应。

总结一下,我们通过在电脑端弹出的调试窗口中点击不同的动作按钮,即可实现对 App 的控制,同时 Recorder 面板可以生成对应的代码。在 Appium 客户端控制和操作 App 的方法就介绍完了。

下面我们看看使用 Python 代码打开 App 的方法,这里需要借助我们已经安装好的 Appium 的 Python 库实现。首先在代码中指定一个 Appium 服务,而这个服务在刚才打开 Appium 的时候就已经开启了,运行在 4723 端口上,配置如下所示:

server = "http://localhost:4723/wd/hub"

接下来用字典配置 DesiredCapabilities 参数,代码如下:

desired_capabilities = {
    "platformName": "Android",
    "deviceName": "SM_G9860",
    "appPackage": "com.goldze.mvvmhabit",
    "appActivity": ".ui.MainActivity",
    "noReset": True
}

新建一个 Session,这和点击 Appium 内置驱动器的 StartSession 按钮功能相同,代码实现如下:

from appium import webdriver
driver = webdriver.Remote(server, desired_capabilities)

配置完成后,运行代码就可以启动示例 App 了,但现在仅仅能启动 App,还不能做任何动作。接下来实现一个加载等待和下拉的逻辑:

from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

wait = WebDriverWait(driver, 30)
wait.until(EC.presence_of_all_elements_located(
    (By.XPATH, '//android.support.v7.widget.RecyclerView/android.widget.LinearLayout')))
window_size = driver.get_window_size()
width,height = window_size.get('width'), window_size.get("height")
driver.swipe(width * 0.5, height * 0.8, width * 0.5, height * 0.2, 1000)

这段代码先确保所有电影条目加载成功。因为一个电影条目对应一个 android.widget.LinearLayout 节点,这些节点的父节点是 android.support.v7.widget.RecyclerView,所以这里构造了一个取值为 //android.support.v7.widget.RecyclerView/android.widget.LinearLayout 的 XPath,用来查找每个电影条目,外层 presence_of_all_elements_located 的意思是确保所有电影条目都加载出来,其外再套一层 WebDriverWait 对象的 until 方法,设置加载的超时时间为 30 秒。于是,最长会等待30秒,如果这期间所有电影条目都加载出来,就立即向下执行,如果没有加载成功,就代表数据加载失败,抛出超时异常。

接着获取了手机页面的宽高信息,然后调用 swipe 方法执行了一次屏幕滑动,这个方法接收5个参数,分别是 x1y1x2y2duration。其中 x1y1 标识了滑动的初始位置,x2y2 标识了滑动的结束位置,分别是两个位置相对屏幕左上角的横纵坐标。左上角的坐标是 (0,0),向右为 x 轴的正方向,向下为 y 轴的正方向。这里设置 x1 为手机页面宽度的 0.5 倍,设置 y1 为手机页面高度的 0.8 倍,x2 同样为手机页面宽度的 0.5 倍,y2 则是手机页面高度的 0.3 倍。duration 是滑动时间,这里设置为 1000(单位为毫秒),即 1 秒。滑动效果如图 12-56 中的红色箭头所示。

图 12-56 滑动效果

我们模拟了垂直向上滑动,触发加载下一页数据的过程。综上所述,完整的代码如下:

from appium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

server = "http://localhost:4723/wd/hub"

desired_capabilities = {
    "platformName": "Android",
    "deviceName": "SMG9860",
    "appPackage": "com.goldze.mvvmhabit",
    "appActivity": ".ui.MainActivity",
    "noReset": True
}

driver = webdriver.Remote(server,desired_capabilities)
wait = WebDriverWait(driver,30)
wait.until(EC.presence_of_all_elements_located(
    (By.XPATH, "//android.support.v7.widget.RecyclerView/android.widget.LinearLayout")))
window_size = driver.get_window_size()
width, height = window_size.get("width"), window_size.get("height")
driver.swipe(width * 0.5, height * 0.8, width * 0.5, height * 0.2, 1000)

重新运行代码,App 会重启,首页的电影数据加载出来之后,屏幕会向上滑动一下,接着第 2 页电影数据成功加载出来。

Appium 的相关 API

本节我们来总结一下 Appium 的相关 API 怎么用。使用的 Python 库是 AppiumPythonClient( https://github.com/appium/python-client ),此库继承自 Selenium,因此使用方法与 Selenium 有很多共同之处。

初始化

需要先配置启动 App 的 Desired Capabilities 参数,完整的配置说明可以参考 https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md ,一般配置几个基本参数即可:

from appium import webdriver

server = 'http://localhost:4723/wd/hub'
desired\_capabilities = {
"platformName": "Android",
"deviceName": "SM\_G9860",
}
"appPackage": "com.goldze.mvvmhabit",
"appActivity": ".ui.MainActivity",
"noReset": True
}
driver = webdriver.Remote(server, desired_capabilities)

这样 Appium 就会自动按照 Desired Capabilities 参数设置的内容查找手机上的包名和入口类,然后将 App 启动。如果没有事先在手机上安装要打开的 App, 可以直接指定参数 app 为安装包所在的路径,这样程序启动时就会自动在手机上安装并启动 App, 代码如下:

from appium import webdriver

server = 'http://localhost:4723/wd/hub'
desired_capabilities = {
    'platformName': 'Android',
    'deviceName': 'SM_G9860',
    'app': './scrape_apps.apk'
}
driver = webdriver.Remote(server, desired_capabilities)

查找节点

以下是图中识别出的文本:

可以使用和 Selenium 类似的通用查找方法来查找节点,代码如下:

el = driver.find_element_by_id('<package>:id/id>')

Selenium 中其他用来查找节点的方法在此处也同样适用,不再赘述。还可以使用 UIAutomator 查找节点,针对 Android 平台的代码如下:

el = self.driver.find_element_by_android_uiautomator('new UiSelector().description("Animation")')
els = self.driver.find_elements_by_android_uiautomator('new UiSelector().clickable(true)')

针对 iOS 平台的代码如下:

el = self.driver.find_element_by_ios_uiautomation('.elements()[0]')
els = self.driver.find_elements_by_ios_uiautomation('.elements()')

此外,使用 iOS Predicates 查找节点的代码如下:

el = self.driver.find_element_by_ios_predicate('wdName == "Buttons"')
els = self.driver.find_elements_by_ios_predicate('wdValue == "SearchBar" AND isWDDivisible == 1')

使用 iOS Class Chain 查找节点的代码如下:

el = self.driver.find_element_by_ios_class_chain('XCUIElementTypeWindow/XCUIElementTypeButton[3]')
els = self.driver.find_elements_by_ios_class_chain('XCUIElementTypeWindow/XCUIElementTypeButton')

注意这种方法只适用于 XCUITest 驱动,具体可以参考 https://github.com/appium/appium-xcuitest-driver

点击屏幕

可以使用 tap 方法模拟点击操作,该方法能够模拟手指点击(最多五个手指),设置和屏幕的接触时长(单位为毫秒),定义如下:

tap(self, positions, duration=None)

参数有 positions 和 duration。

  • positions: 点击位置组成的列表。

  • duration: 点击持续的时间。

实例如下:

driver.tap([(100, 20), (100, 60), (100, 100)], 500)

运行这段代码,就可以模拟点击手机页面中几个指定位置的点。另外,我们可以直接调用 cilck 方法模拟点击某个节点(如按钮)的操作,实例如下:

button = find_element_by_id('<package>:id/<id>')
button.click()

这里先获取节点,然后调用 click 方法模拟点击该节点。

屏幕滑动

以下是图中识别出的文本:

可以使用 scroll 方法模拟屏幕滑动,其定义如下:

scroll(self, origin_el, destination_el)

表示从元素 origin_el 滑动至元素 destination_el

实例如下:

driver.scroll(el1, el2)

还可以使用 swipe 方法模拟从 A 点滑动到 B 点的动作,这个方法之前已经应用过,其定义如下:

swipe(self, start_x, start_y, end_x, end_y, duration=None)

参数有 start_x, start_y, end_x, end_yduration

  • start_x: 开始位置的横坐标。

  • start_y: 开始位置的纵坐标。

  • end_x: 结束位置的横坐标。

  • end_y: 结束位置的纵坐标。

  • duration: 持续时间,单位为毫秒。

实例如下:

driver.swipe(100, 100, 100, 400, 5000)

运行这段代码,可以在 5 秒内由点 (100, 100) 滑动到点 (100, 400)。另外可以使用 flick 方法模拟从 A 点快速滑动到 B 点的动作。用法如下:

flick(self, start_x, start_y, end_x, end_y)

参数有 start_x, start_y, end_xend_y

  • start_x: 开始位置的横坐标。

  • start_y: 开始位置的纵坐标。

  • end_x: 结束位置的横坐标。

  • end_y: 结束位置的纵坐标。

实例如下:

driver.flick(100, 100, 100, 400)

拖动

以下是图中识别出的文本:

可以使用 drag_and_drop 方法模拟把一个节点拖动到另一个节点处的动作,其用法如下:

drag_and_drop(self, origin_el, destination_el)

可以把节点 origin_el 拖动到节点 destination_el 处。

参数有 origin_eldestination_el

  • origin_el: 被拖动的节点。

  • destination_el: 目标节点。

实例如下:

driver.drag_and_drop(el1, el2)

文本输入

以下是图中识别出的文本:

可以使用 set_text 方法模拟文本输入,实例如下:

el = find_element_by_id('<package>:id/cjk')
el.set_text('Hello')

这里先选中一个文本框元素,然后调用 set_text 方法输入文本。

动作链

以下是图中识别出的文本:

与 Selenium 中的 ActionChains 类似,Appium 中的 TouchAction 可支持 tappresslong_pressreleasemove_towaitcancel 等方法,实例如下:

el = self.driver.find_element_by_accessibility_id('Animation')
action = TouchAction(self.driver)
action.tap(el).perform()

这里首先选中一个节点,然后利用 TouchAction 点击此节点。如果想实现拖动操作,可以这样:

els = self.driver.find_elements_by_class_name('listView')
a1 = TouchAction()
a1.press(els[0]).move_to(x=10, y=0).move_to(x=10, y=-75).move_to(x=10, y=-600).release()
a2 = TouchAction()
a2.press(els[1]).move_to(x=10, y=10).move_to(x=10, y=-300).move_to(x=10, y=-600).release()

利用本节所讲的 API, 可以完成绝大部分自动化操作。更多的 API 详情可以参考 https://appium.io/docs/en/about-appium/api/

总结

本节我们主要了解了 Appium 操作 App 的基本用法,以及常用 API 的用法,在 12.5 节我们会用一个实例演示 Appium 的使用方法。