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,对于不同的手机其结果不同,请以自己的结果为准。
|
这步一定是成功获取了设备信息才能证明电脑和手机连接成功了。如果提示找不到 |
接下来用 Appium 内置的驱动器打开 App,点击运行界面右上方的 Start Inspector Session 按钮,如图 12-44 所示。
图 12-44 操作示例
会打开一个配置页面,如图 12-45 所示。
我们需要在这里配置启动 App 时的 DesiredCapabilities 参数,包括 platformName、deviceName、 appPackage 和 appActivity。
-
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 电脑端弹出的调试窗口
点击左栏中手机页面的某个元素,它就会高亮显示,如这里的电影名称 “霸王别姬”。这时中间栏会显示它对应的源代码;右栏会显示它的基本信息,如 index、class、text 等。我们还可以在右栏执行一些操作,如 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个参数,分别是 x1、y1、x2、y2、duration。其中 x1、y1 标识了滑动的初始位置,x2、y2 标识了滑动的结束位置,分别是两个位置相对屏幕左上角的横纵坐标。左上角的坐标是 (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_y 和 duration。
-
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_x 和 end_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_el 和 destination_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 可支持 tap、press、long_press、release、move_to、wait、cancel 等方法,实例如下:
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/ 。