原文地址:https://blog.coding.net/blog/...node
Appium 是一個 C/S 架構的,支持 Android/iOS Native, Hybrid 和 Mobile Web Apps 的測試框架,與測試程序經過 Selenum Webdriver 協議通信。Webdriver 的好處是經過 HTTP RPC 的方式調用 Server 上的過程,編寫測試腳本不受語言的限制,不管是 Python, Java, NodeJS 都可以方便的編寫測試。本文中將使用 Python 進行編程。python
原由是由於市場部的同事拋來以下需求:批量添加一些微信好友。直接抓取請求進行重放的方法是不靠譜的,微信與服務端的通信均加密,Pass。考慮使用 xposed 等框架 hook 相關函數進行操做。可是 xposed 須要越獄,且開發複雜,Pass。後來想到了使用 UI 測試工具進行模擬操做,開發較爲簡單。android
Android UI 測試工具備不少種,如 Monkey, UIAutomator, Selendroid, Robotium 等。其中 UIAutomator, Monkey, Selendroid 均爲非侵入式的 UI 測試,也就是不須要修改源代碼,只要安裝了目標程序就能夠進行測試。Robotium 須要與源碼一同編譯測試。Appium 實際上就是一個測試工具的統一調度軟件,將不一樣的非侵入式測試工具整合在一塊兒,對外提供統一的 API。在 Android 2.3 之前的版本,Appium 會調用 Selendroid ,以後的版本會直接使用 UIAutomator,iOS 下使用 UIAutomation。Appium 還支持 FirefoxOS 的 UI 測試。web
官網給出了命令行下的安裝方法。但實際上 Appium 有 GUI 版本,更適合在 Windows/MacOS 下使用。Windows 下須要安裝 .NET Framework。npm
> brew install node # get node.js > npm install -g appium # get appium > npm install wd # get appium client > appium & # start appium > node your-appium-test.js
Appium 須要依賴 Android SDK 編譯在手機端運行的兩個插件,所以須要首先安裝相應的 Android SDK 版本。這裏直接使用了 Android Studio 中自帶的 SDK Manager。在 SDK Manager 中選擇和測試機相對應的 SDK Platform 和較新的 Build-tools,若是須要使用模擬器測試還要裝對應的 ARM/x86 System Image,以及 Intel HAXM Installer,用於加速 x86 虛擬機。Appium 使用 adb 來與目標機器通信,所以對於真機和模擬器操做幾乎都是相同的,如何創建模擬器在此再也不贅述。編程
安裝完成後須要在 Appium GUI 中配置 Android SDK 目錄,隨後選擇 Android,點擊 Launch 就能夠啓動 Appium Server。微信
Appium Server 默認會監聽 http://localhost:4723 ,用於 RPC 通信。下面咱們就能夠打開熟悉的編程環境,編寫 UI 測試用例了。這裏使用 Python 進行編寫,須要先安裝 Appium 的 Python Client ,而後再 python 中使用 appium.webclient 就能夠鏈接 Appium server了。架構
pip install Appium-Python-Client
根據註釋修改相應屬性後便可運行測試。手機須要打開 ADB 調試,執行完如下代碼後,Appium 會在手機上安裝 Appium Settings 和 Unlock 兩個程序,隨後微信會被啓動。app
from appium import webdriver desired_caps = {} desired_caps['platformName'] = 'Android' #測試平臺 desired_caps['platformVersion'] = '5.1' #平臺版本 desired_caps['deviceName'] = 'm3_note' #設備名稱,多設備時需區分 desired_caps['appPackage'] = 'com.tencent.mm' #app package名 desired_caps['appActivity'] = '.ui.LauncherUI' #app默認Activity dr = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) #啓動Remote RPC
Selenum Webdriver 使用了一種相似於 JS 中的 DOM 模型的方法來選擇頁面中的元素。dr 爲當前正在活動的 activity 對象,可使用 findElementByXXX 的方法來獲取 Activity 中的元素。全部 Element 後帶 s 的函數,均得到全部匹配的元素,不帶 s 的函數得到第一個匹配的元素。框架
在 Android 中基本沒用。Android UI 沒有 Name 這個屬性。有說可使用 text 值獲取。但我並無成功
經過類名來獲取元素,用法以下:
item_list = dr.find_elements_by_class_name("android.widget.LinearLayout") item_list[2].click()
經過 resource_id 來獲取元素,每一個 Activity 中都是惟一的,用法以下
t = dr.find_element_by_id("com.tencent.mm:id/f7") t.send_keys(wechatId)
在 Android 上 AccessbilityID 實際就是 contentDescription 。這個屬性是爲了方便視力受損人士使用手機所設置。開啓 TTS 後系統會朗讀相關控件的 contentDescription。
經過 XML Path 描述來尋找元素。我沒有成功的獲取到,多是 XPath 寫的有問題。
s = dr.find_element_by_xpath("//android.widget.TextView[contains(@text,'搜索')]") s.click()
經過 UIAutomator 的選擇器來獲取元素。由於 Appium 在 Android 上實際是調用的 UIAutomator,因此能夠經過 UIAutomator 的選擇器來選擇元素。
el = dr.find_element_by_android_ui_automator("new UiSelector().text(\"搜索\")") el.click()
操做函數用於操做選定的元素,有不少,如下僅列舉幾個,更多的請查閱手冊。
click
send_keys
clear
查詢函數返回的元素對象能夠像 JS 中的 dom 元素同樣,繼續使用查詢函數來選定其子元素。用例以下。
search = dr.find_element_by_id("com.tencent.mm:id/aqw").find_element_by_class_name("android.widget.RelativeLayout") search.click()
瞭解了相關的函數後,下面就應對 UI 進行定位了。若是是本身團隊開發的程序,推薦讓開發同窗在全部的空間上都添加 resource_id 進行絕對定位。若是碰到沒有談價 resource_id 的元素,那就要使用別的辦法進行定位了。
UI Automator Viewer 是 Android 官方的 UI 定位工具,位於 sdk/tools 下。運行後會打開 viewer 界面。點擊獲取按鈕便可獲取當前正在運行的 Activity 的 UI 結構。
AppiumDriver(Client) 能夠很方便的得到當前正在運行的 Activity 的 UI 描述,隨後可根據返回的 XML 文檔來尋找元素。
print dr.page_source
肯定元素位置後,便可根據前述的 Find 方法來查找/選擇元素
正確的獲取元素以後即可以獲取元素相關的信息,隨後使用各語言經常使用的測試框架編寫測試便可,如 Java 的 JUnit,Nodejs 的 Mocha 等。
這裏我使用 Appium 主要是爲了模擬用戶點擊添加微信好友,因此完整的程序並無使用到測試框架。相關的 UI 元素獲取/操做方法供你們參考。
# coding:utf-8 from appium import webdriver from time import sleep def addFriend(dr, id, dryRun=False): succ = False wechatId = str(id) dr.find_element_by_accessibility_id(r"更多功能按鈕").click() item_list = dr.find_elements_by_class_name("android.widget.LinearLayout") try: item_list[2].click() except: print "Error! in item list len" return succ el = dr.find_element_by_class_name("android.widget.ListView") item_list = el.find_elements_by_class_name("android.widget.LinearLayout") try: item_list[1].click() except: print "Error! in item list len" return succ t = dr.find_element_by_id("com.tencent.mm:id/f7") t.send_keys(wechatId) search = dr.find_element_by_id("com.tencent.mm:id/aqw").find_element_by_class_name("android.widget.RelativeLayout") search.click() try: freq = dr.find_element_by_id('com.tencent.mm:id/aqq') assert freq.text == u"操做過於頻繁,請稍後再試。" print "Frequency too high! Sleep 300s" sleep(60) return succ except: pass try: dr.find_element_by_id('com.tencent.mm:id/a8x').click() addBtn = dr.find_element_by_id('com.tencent.mm:id/eu') if not dryRun: addBtn.click() succ = True print "Success Send Requests:" + wechatId except: print "No Such User Or Already a Friend:" + wechatId while True: try: dr.find_element_by_id('com.tencent.mm:id/fb').click() except: try: dr.find_element_by_id('com.tencent.mm:id/f4').click() except: break return True def resetActivity(dr, desired_caps): dr.start_activity(desired_caps['appPackage'], desired_caps['appActivity']) desired_caps = {} desired_caps['platformName'] = 'Android' desired_caps['platformVersion'] = '5.1' desired_caps['deviceName'] = 'm3_note' desired_caps['appPackage'] = 'com.tencent.mm' desired_caps['appActivity'] = '.ui.LauncherUI' print "Trying connect to phone..." dr = {} try: dr = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) except Exception, e: print "Cannot Connect to phone :", e exit() print "Successfully connect to phone." print "Reading friend list..." friendList = [] fp = open("friends.txt") line = fp.readline().strip() while line: friendList.append(line) line = fp.readline().strip() print "Finish reading friends. Total: " + str(len(friendList)) print "Wait for Wechat's splash screen...." for i in range(0, 10): print 10 - i sleep(1) succ_list = [] fail_list = [] for i in friendList: try: succ = addFriend(dr, i, dryRun=False) if succ: succ_list.append(i) else: fail_list.append(i) except: fail_list.append(i) resetActivity(dr, desired_caps) print "Succeed List:" print "\n".join(succ_list) print "Failed List:" print "\n".join(fail_list) dr.close()